mirror of
https://github.com/google/santa.git
synced 2026-01-18 02:37:53 -05:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
221664436f | ||
|
|
65c660298c | ||
|
|
2b5d55781c | ||
|
|
84e6d6ccff | ||
|
|
c16f90f5f9 | ||
|
|
d503eae4d9 | ||
|
|
818518bb38 | ||
|
|
f499654951 | ||
|
|
a5e8d77d06 | ||
|
|
edac42e8b8 | ||
|
|
ce5e3d0ee4 | ||
|
|
3e51ec6b8a | ||
|
|
ed227f43d4 | ||
|
|
056ed75bf1 | ||
|
|
8f5f8de245 | ||
|
|
7c58648c35 | ||
|
|
3f3751eb18 | ||
|
|
7aa2d69ce6 | ||
|
|
f9a937a6e4 | ||
|
|
d2cbddd3fb | ||
|
|
ea7e11fc22 | ||
|
|
7530b8f5c1 | ||
|
|
64bb34b2ca | ||
|
|
c5c6037085 | ||
|
|
275a8ed607 | ||
|
|
28dd6cbaed | ||
|
|
8c466b4408 | ||
|
|
373c676306 | ||
|
|
d214d510e5 | ||
|
|
6314fe04e3 | ||
|
|
11d9c29daa | ||
|
|
60238f0ed2 | ||
|
|
7aa731a76f |
9
.github/workflows/check-markdown.yml
vendored
9
.github/workflows/check-markdown.yml
vendored
@@ -9,6 +9,9 @@ jobs:
|
||||
markdown-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
- run: "! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'"
|
||||
- name: "Checkout Santa"
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # ratchet:actions/checkout@master
|
||||
- name: "Check for deadlinks"
|
||||
uses: gaurav-nelson/github-action-markdown-link-check@5c5dfc0ac2e225883c0e5f03a85311ec2830d368 # ratchet:gaurav-nelson/github-action-markdown-link-check@v1
|
||||
- name: "Check for trailing whitespace and newlines"
|
||||
run: "! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'"
|
||||
|
||||
22
.github/workflows/e2e.yml
vendored
22
.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
|
||||
@@ -31,11 +34,16 @@ jobs:
|
||||
bazel run //Testing/integration:allow_sysex
|
||||
sudo santactl sync --debug
|
||||
- name: Run integration test binaries
|
||||
run: bazel test //Testing/integration:integration_tests
|
||||
run: |
|
||||
bazel test //Testing/integration:integration_tests
|
||||
sleep 3
|
||||
bazel run //Testing/integration:dismiss_santa_popup || true
|
||||
- name: Test config changes
|
||||
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
|
||||
|
||||
@@ -40,6 +40,12 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTDeepCopy",
|
||||
srcs = ["SNTDeepCopy.m"],
|
||||
hdrs = ["SNTDeepCopy.h"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "SantaCache",
|
||||
hdrs = ["SantaCache.h"],
|
||||
|
||||
@@ -74,6 +74,11 @@ class PrefixTree {
|
||||
node_count_ = 0;
|
||||
}
|
||||
|
||||
uint32_t NodeCount() {
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
return node_count_;
|
||||
}
|
||||
|
||||
#if SANTA_PREFIX_TREE_DEBUG
|
||||
void Print() {
|
||||
char buf[max_depth_ + 1];
|
||||
@@ -82,11 +87,6 @@ class PrefixTree {
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
PrintLocked(root_, buf, 0);
|
||||
}
|
||||
|
||||
uint32_t NodeCount() {
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
return node_count_;
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
@property NSArray<MOLCertificate *> *certChain;
|
||||
@property NSString *teamID;
|
||||
@property NSString *signingID;
|
||||
@property NSDictionary *entitlements;
|
||||
@property BOOL entitlementsFiltered;
|
||||
|
||||
@property NSString *quarantineURL;
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -628,6 +642,18 @@
|
||||
///
|
||||
@property(readonly, nonatomic) NSUInteger metricExportTimeout;
|
||||
|
||||
///
|
||||
/// List of prefix strings for which individual entitlement keys with a matching
|
||||
/// prefix should not be logged.
|
||||
///
|
||||
@property(readonly, nonatomic) NSArray<NSString *> *entitlementsPrefixFilter;
|
||||
|
||||
///
|
||||
/// List of TeamIDs for which entitlements should not be logged. Use the string
|
||||
/// "platform" to refer to platform binaries.
|
||||
///
|
||||
@property(readonly, nonatomic) NSArray<NSString *> *entitlementsTeamIDFilter;
|
||||
|
||||
///
|
||||
/// Retrieve an initialized singleton configurator object using the default file path.
|
||||
///
|
||||
|
||||
@@ -20,6 +20,21 @@
|
||||
#import "Source/common/SNTStrengthify.h"
|
||||
#import "Source/common/SNTSystemInfo.h"
|
||||
|
||||
// Ensures the given object is an NSArray and only contains NSString value types
|
||||
static NSArray<NSString *> *EnsureArrayOfStrings(id obj) {
|
||||
if (![obj isKindOfClass:[NSArray class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
for (id item in obj) {
|
||||
if (![item isKindOfClass:[NSString class]]) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@interface SNTConfigurator ()
|
||||
/// A NSUserDefaults object set to use the com.google.santa suite.
|
||||
@property(readonly, nonatomic) NSUserDefaults *defaults;
|
||||
@@ -116,11 +131,15 @@ static NSString *const kFCMProject = @"FCMProject";
|
||||
static NSString *const kFCMEntity = @"FCMEntity";
|
||||
static NSString *const kFCMAPIKey = @"FCMAPIKey";
|
||||
|
||||
static NSString *const kEntitlementsPrefixFilterKey = @"EntitlementsPrefixFilter";
|
||||
static NSString *const kEntitlementsTeamIDFilterKey = @"EntitlementsTeamIDFilter";
|
||||
|
||||
// The keys managed by a sync server or mobileconfig.
|
||||
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 +200,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kBlockedPathRegexKeyDeprecated : re,
|
||||
kBlockUSBMountKey : number,
|
||||
kRemountUSBModeKey : array,
|
||||
kOnStartUSBOptions : string,
|
||||
kEnablePageZeroProtectionKey : number,
|
||||
kEnableBadSignatureProtectionKey : number,
|
||||
kEnableSilentModeKey : number,
|
||||
@@ -238,6 +258,8 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kEnableAllEventUploadKey : number,
|
||||
kDisableUnknownEventUploadKey : number,
|
||||
kOverrideFileAccessActionKey : string,
|
||||
kEntitlementsPrefixFilterKey : array,
|
||||
kEntitlementsTeamIDFilterKey : array,
|
||||
};
|
||||
_defaults = [NSUserDefaults standardUserDefaults];
|
||||
[_defaults addSuiteNamed:@"com.google.santa"];
|
||||
@@ -525,6 +547,14 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self syncAndConfigStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEntitlementsPrefixFilter {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEntitlementsTeamIDFilter {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
#pragma mark Public Interface
|
||||
|
||||
- (SNTClientMode)clientMode {
|
||||
@@ -635,6 +665,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;
|
||||
}
|
||||
@@ -1093,6 +1139,14 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
self.syncState = [NSMutableDictionary dictionary];
|
||||
}
|
||||
|
||||
- (NSArray *)entitlementsPrefixFilter {
|
||||
return EnsureArrayOfStrings(self.configState[kEntitlementsPrefixFilterKey]);
|
||||
}
|
||||
|
||||
- (NSArray *)entitlementsTeamIDFilter {
|
||||
return EnsureArrayOfStrings(self.configState[kEntitlementsTeamIDFilterKey]);
|
||||
}
|
||||
|
||||
#pragma mark Private Defaults Methods
|
||||
|
||||
- (NSRegularExpression *)expressionForPattern:(NSString *)pattern {
|
||||
|
||||
27
Source/common/SNTDeepCopy.h
Normal file
27
Source/common/SNTDeepCopy.h
Normal file
@@ -0,0 +1,27 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface NSArray (SNTDeepCopy)
|
||||
|
||||
- (instancetype)sntDeepCopy;
|
||||
|
||||
@end
|
||||
|
||||
@interface NSDictionary (SNTDeepCopy)
|
||||
|
||||
- (instancetype)sntDeepCopy;
|
||||
|
||||
@end
|
||||
53
Source/common/SNTDeepCopy.m
Normal file
53
Source/common/SNTDeepCopy.m
Normal file
@@ -0,0 +1,53 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTDeepCopy.h"
|
||||
|
||||
@implementation NSArray (SNTDeepCopy)
|
||||
|
||||
- (instancetype)sntDeepCopy {
|
||||
NSMutableArray<__kindof NSObject *> *deepCopy = [NSMutableArray arrayWithCapacity:self.count];
|
||||
for (id object in self) {
|
||||
if ([object respondsToSelector:@selector(sntDeepCopy)]) {
|
||||
[deepCopy addObject:[object sntDeepCopy]];
|
||||
} else if ([object respondsToSelector:@selector(copyWithZone:)]) {
|
||||
[deepCopy addObject:[object copy]];
|
||||
} else {
|
||||
[deepCopy addObject:object];
|
||||
}
|
||||
}
|
||||
return deepCopy;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSDictionary (SNTDeepCopy)
|
||||
|
||||
- (instancetype)sntDeepCopy {
|
||||
NSMutableDictionary<__kindof NSObject *, __kindof NSObject *> *deepCopy =
|
||||
[NSMutableDictionary dictionary];
|
||||
for (id key in self) {
|
||||
id value = self[key];
|
||||
if ([value respondsToSelector:@selector(sntDeepCopy)]) {
|
||||
deepCopy[key] = [value sntDeepCopy];
|
||||
} else if ([value respondsToSelector:@selector(copyWithZone:)]) {
|
||||
deepCopy[key] = [value copy];
|
||||
} else {
|
||||
deepCopy[key] = value;
|
||||
}
|
||||
}
|
||||
return deepCopy;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -71,6 +71,8 @@
|
||||
- (void)syncCleanRequired:(void (^)(BOOL))reply;
|
||||
- (void)enableBundles:(void (^)(BOOL))reply;
|
||||
- (void)enableTransitiveRules:(void (^)(BOOL))reply;
|
||||
- (void)blockUSBMount:(void (^)(BOOL))reply;
|
||||
- (void)remountUSBMode:(void (^)(NSArray<NSString *> *))reply;
|
||||
|
||||
///
|
||||
/// Metrics ops
|
||||
|
||||
@@ -245,7 +245,7 @@ struct S {
|
||||
uint64_t first_val;
|
||||
uint64_t second_val;
|
||||
|
||||
bool operator==(const S &rhs) {
|
||||
bool operator==(const S &rhs) const {
|
||||
return first_val == rhs.first_val && second_val == rhs.second_val;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -92,7 +92,11 @@ es_process_t MakeESProcess(es_file_t *file, audit_token_t tok, audit_token_t par
|
||||
}
|
||||
|
||||
uint32_t MaxSupportedESMessageVersionForCurrentOS() {
|
||||
// Note: ES message v3 was only in betas.
|
||||
// Notes:
|
||||
// 1. ES message v3 was only in betas.
|
||||
// 2. Message version 7 appeared in macOS 13.3, but features from that are
|
||||
// not currently used. Leaving off support here so as to not require
|
||||
// adding v7 test JSON files.
|
||||
if (@available(macOS 13.0, *)) {
|
||||
return 6;
|
||||
} else if (@available(macOS 12.3, *)) {
|
||||
|
||||
@@ -213,6 +213,27 @@ message CertificateInfo {
|
||||
optional string common_name = 2;
|
||||
}
|
||||
|
||||
// Information about a single entitlement key/value pair
|
||||
message Entitlement {
|
||||
// The name of an entitlement
|
||||
optional string key = 1;
|
||||
|
||||
// The value of an entitlement
|
||||
optional string value = 2;
|
||||
}
|
||||
|
||||
// Information about entitlements
|
||||
message EntitlementInfo {
|
||||
// Whether or not the set of reported entilements is complete or has been
|
||||
// filtered (e.g. by configuration or clipped because too many to log).
|
||||
optional bool entitlements_filtered = 1;
|
||||
|
||||
// The set of entitlements associated with the target executable
|
||||
// Only top level keys are represented
|
||||
// Values (including nested keys) are JSON serialized
|
||||
repeated Entitlement entitlements = 2;
|
||||
}
|
||||
|
||||
// Information about a process execution event
|
||||
message Execution {
|
||||
// The process that executed the new image (e.g. the process that called
|
||||
@@ -286,6 +307,9 @@ message Execution {
|
||||
// The original path on disk of the target executable
|
||||
// Applies when executables are translocated
|
||||
optional string original_path = 15;
|
||||
|
||||
// Entitlement information about the target executbale
|
||||
optional EntitlementInfo entitlement_info = 16;
|
||||
}
|
||||
|
||||
// Information about a fork event
|
||||
@@ -381,6 +405,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 +558,7 @@ message SantaMessage {
|
||||
Bundle bundle = 19;
|
||||
Allowlist allowlist = 20;
|
||||
FileAccess file_access = 21;
|
||||
CodesigningInvalidated codesigning_invalidated = 22;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -35,10 +35,6 @@ macos_command_line_application(
|
||||
],
|
||||
infoplists = ["Info.plist"],
|
||||
minimum_os_version = "11.0",
|
||||
provisioning_profile = select({
|
||||
"//:adhoc_build": None,
|
||||
"//conditions:default": "//profiles:santa_dev",
|
||||
}),
|
||||
version = "//:version",
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [":santabs_lib"],
|
||||
|
||||
@@ -92,10 +92,6 @@ macos_command_line_application(
|
||||
],
|
||||
infoplists = ["Info.plist"],
|
||||
minimum_os_version = "11.0",
|
||||
provisioning_profile = select({
|
||||
"//:adhoc_build": None,
|
||||
"//conditions:default": "//profiles:santa_dev",
|
||||
}),
|
||||
version = "//:version",
|
||||
deps = [":santactl_lib"],
|
||||
)
|
||||
@@ -115,6 +111,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
|
||||
|
||||
@@ -45,7 +56,6 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
|
||||
|
||||
// Daemon status
|
||||
@@ -158,10 +168,15 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
}
|
||||
}];
|
||||
|
||||
// Wait a maximum of 5s for stats collected from daemon to arrive.
|
||||
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5))) {
|
||||
fprintf(stderr, "Failed to retrieve some stats from daemon\n\n");
|
||||
}
|
||||
__block BOOL blockUSBMount = NO;
|
||||
[rop blockUSBMount:^(BOOL response) {
|
||||
blockUSBMount = response;
|
||||
}];
|
||||
|
||||
__block NSArray<NSString *> *remountUSBMode;
|
||||
[rop remountUSBMode:^(NSArray<NSString *> *response) {
|
||||
remountUSBMode = response;
|
||||
}];
|
||||
|
||||
// Format dates
|
||||
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
||||
@@ -191,10 +206,9 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"watchdog_ram_events" : @(ramEvents),
|
||||
@"watchdog_cpu_peak" : @(cpuPeak),
|
||||
@"watchdog_ram_peak" : @(ramPeak),
|
||||
@"block_usb" : @(configurator.blockUSBMount),
|
||||
@"remount_usb_mode" : (configurator.blockUSBMount && configurator.remountUSBMode.count
|
||||
? configurator.remountUSBMode
|
||||
: @""),
|
||||
@"block_usb" : @(blockUSBMount),
|
||||
@"remount_usb_mode" : (blockUSBMount && remountUSBMode.count ? remountUSBMode : @""),
|
||||
@"on_start_usb_options" : StartupOptionToString(configurator.onStartUSBOptions),
|
||||
},
|
||||
@"database" : @{
|
||||
@"binary_rules" : @(binaryRuleCount),
|
||||
@@ -250,11 +264,13 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
printf(" %-25s | %s\n", "Mode", [clientMode UTF8String]);
|
||||
printf(" %-25s | %s\n", "Log Type", [eventLogType UTF8String]);
|
||||
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:",
|
||||
[[configurator.remountUSBMode componentsJoinedByString:@", "] UTF8String]);
|
||||
printf(" %-25s | %s\n", "USB Blocking", (blockUSBMount ? "Yes" : "No"));
|
||||
if (blockUSBMount && remountUSBMode.count > 0) {
|
||||
printf(" %-25s | %s\n", "USB Remounting Mode",
|
||||
[[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 {
|
||||
|
||||
@@ -199,6 +199,7 @@ objc_library(
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDeepCopy",
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTRule",
|
||||
@@ -236,10 +237,12 @@ objc_library(
|
||||
":SNTSyncdQueue",
|
||||
":TTYWriter",
|
||||
"//Source/common:BranchPrediction",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTBlockMessage",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDeepCopy",
|
||||
"//Source/common:SNTDropRootPrivs",
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
@@ -248,7 +251,9 @@ objc_library(
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:String",
|
||||
"//Source/common:Unit",
|
||||
"@MOLCodesignChecker",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -395,8 +400,10 @@ objc_library(
|
||||
":Metrics",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTDeviceEvent",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1317,6 +1324,7 @@ santa_unit_test(
|
||||
":Metrics",
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTEndpointSecurityDeviceManager",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDeviceEvent",
|
||||
"//Source/common:TestUtils",
|
||||
|
||||
@@ -41,6 +41,8 @@ enum class FlushCacheReason {
|
||||
kStaticRulesChanged,
|
||||
kExplicitCommand,
|
||||
kFilesystemUnmounted,
|
||||
kEntitlementsPrefixFilterChanged,
|
||||
kEntitlementsTeamIDFilterChanged,
|
||||
};
|
||||
|
||||
class AuthResultCache {
|
||||
|
||||
@@ -31,6 +31,10 @@ static NSString *const kFlushCacheReasonRulesChanged = @"RulesChanged";
|
||||
static NSString *const kFlushCacheReasonStaticRulesChanged = @"StaticRulesChanged";
|
||||
static NSString *const kFlushCacheReasonExplicitCommand = @"ExplicitCommand";
|
||||
static NSString *const kFlushCacheReasonFilesystemUnmounted = @"FilesystemUnmounted";
|
||||
static NSString *const kFlushCacheReasonEntitlementsPrefixFilterChanged =
|
||||
@"EntitlementsPrefixFilterChanged";
|
||||
static NSString *const kFlushCacheReasonEntitlementsTeamIDFilterChanged =
|
||||
@"EntitlementsTeamIDFilterChanged";
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
@@ -59,6 +63,10 @@ NSString *const FlushCacheReasonToString(FlushCacheReason reason) {
|
||||
case FlushCacheReason::kStaticRulesChanged: return kFlushCacheReasonStaticRulesChanged;
|
||||
case FlushCacheReason::kExplicitCommand: return kFlushCacheReasonExplicitCommand;
|
||||
case FlushCacheReason::kFilesystemUnmounted: return kFlushCacheReasonFilesystemUnmounted;
|
||||
case FlushCacheReason::kEntitlementsPrefixFilterChanged:
|
||||
return kFlushCacheReasonEntitlementsPrefixFilterChanged;
|
||||
case FlushCacheReason::kEntitlementsTeamIDFilterChanged:
|
||||
return kFlushCacheReasonEntitlementsTeamIDFilterChanged;
|
||||
default:
|
||||
[NSException raise:@"Invalid reason"
|
||||
format:@"Unknown reason value: %d", static_cast<int>(reason)];
|
||||
|
||||
@@ -230,13 +230,16 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
{FlushCacheReason::kStaticRulesChanged, @"StaticRulesChanged"},
|
||||
{FlushCacheReason::kExplicitCommand, @"ExplicitCommand"},
|
||||
{FlushCacheReason::kFilesystemUnmounted, @"FilesystemUnmounted"},
|
||||
{FlushCacheReason::kEntitlementsPrefixFilterChanged, @"EntitlementsPrefixFilterChanged"},
|
||||
{FlushCacheReason::kEntitlementsTeamIDFilterChanged, @"EntitlementsTeamIDFilterChanged"},
|
||||
};
|
||||
|
||||
for (const auto &kv : reasonToString) {
|
||||
XCTAssertEqualObjects(FlushCacheReasonToString(kv.first), kv.second);
|
||||
}
|
||||
|
||||
XCTAssertThrows(FlushCacheReasonToString((FlushCacheReason)12345));
|
||||
XCTAssertThrows(FlushCacheReasonToString(
|
||||
(FlushCacheReason)(static_cast<int>(FlushCacheReason::kEntitlementsTeamIDFilterChanged) + 1)));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -322,11 +322,14 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
// Ensure all paths are attempted to be muted even if some fail.
|
||||
// Ensure if any paths fail the overall result is false.
|
||||
EXPECT_CALL(*mockESApi, MuteTargetPath(testing::_, "a", WatchItemPathType::kLiteral))
|
||||
EXPECT_CALL(*mockESApi,
|
||||
MuteTargetPath(testing::_, std::string_view("a"), WatchItemPathType::kLiteral))
|
||||
.WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, MuteTargetPath(testing::_, "b", WatchItemPathType::kLiteral))
|
||||
EXPECT_CALL(*mockESApi,
|
||||
MuteTargetPath(testing::_, std::string_view("b"), WatchItemPathType::kLiteral))
|
||||
.WillOnce(testing::Return(false));
|
||||
EXPECT_CALL(*mockESApi, MuteTargetPath(testing::_, "c", WatchItemPathType::kPrefix))
|
||||
EXPECT_CALL(*mockESApi,
|
||||
MuteTargetPath(testing::_, std::string_view("c"), WatchItemPathType::kPrefix))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
std::vector<std::pair<std::string, WatchItemPathType>> paths = {
|
||||
@@ -349,11 +352,14 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
// Ensure all paths are attempted to be unmuted even if some fail.
|
||||
// Ensure if any paths fail the overall result is false.
|
||||
EXPECT_CALL(*mockESApi, UnmuteTargetPath(testing::_, "a", WatchItemPathType::kLiteral))
|
||||
EXPECT_CALL(*mockESApi,
|
||||
UnmuteTargetPath(testing::_, std::string_view("a"), WatchItemPathType::kLiteral))
|
||||
.WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteTargetPath(testing::_, "b", WatchItemPathType::kLiteral))
|
||||
EXPECT_CALL(*mockESApi,
|
||||
UnmuteTargetPath(testing::_, std::string_view("b"), WatchItemPathType::kLiteral))
|
||||
.WillOnce(testing::Return(false));
|
||||
EXPECT_CALL(*mockESApi, UnmuteTargetPath(testing::_, "c", WatchItemPathType::kPrefix))
|
||||
EXPECT_CALL(*mockESApi,
|
||||
UnmuteTargetPath(testing::_, std::string_view("c"), WatchItemPathType::kPrefix))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
std::vector<std::pair<std::string, WatchItemPathType>> paths = {
|
||||
|
||||
@@ -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;
|
||||
@@ -70,6 +71,9 @@ namespace pbv1 = ::santa::pb::v1;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
static constexpr NSUInteger kMaxEncodeObjectEntries = 64;
|
||||
static constexpr NSUInteger kMaxEncodeObjectLevels = 5;
|
||||
|
||||
std::shared_ptr<Protobuf> Protobuf::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTDecisionCache *decision_cache, bool json) {
|
||||
return std::make_shared<Protobuf>(esapi, std::move(decision_cache), json);
|
||||
@@ -448,6 +452,124 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExchange &msg) {
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
id StandardizedNestedObjects(id obj, int level) {
|
||||
if (level-- == 0) {
|
||||
return [obj description];
|
||||
}
|
||||
|
||||
if ([obj isKindOfClass:[NSNumber class]] || [obj isKindOfClass:[NSString class]]) {
|
||||
return obj;
|
||||
} else if ([obj isKindOfClass:[NSArray class]]) {
|
||||
NSMutableArray *arr = [NSMutableArray array];
|
||||
for (id item in obj) {
|
||||
[arr addObject:StandardizedNestedObjects(item, level)];
|
||||
}
|
||||
return arr;
|
||||
} else if ([obj isKindOfClass:[NSDictionary class]]) {
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
||||
for (id key in obj) {
|
||||
[dict setObject:StandardizedNestedObjects(obj[key], level) forKey:key];
|
||||
}
|
||||
return dict;
|
||||
} else if ([obj isKindOfClass:[NSData class]]) {
|
||||
return [obj base64EncodedStringWithOptions:0];
|
||||
} else if ([obj isKindOfClass:[NSDate class]]) {
|
||||
return [NSISO8601DateFormatter stringFromDate:obj
|
||||
timeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]
|
||||
formatOptions:NSISO8601DateFormatWithFractionalSeconds |
|
||||
NSISO8601DateFormatWithInternetDateTime];
|
||||
|
||||
} else {
|
||||
LOGW(@"Unexpected object encountered: %@", obj);
|
||||
return [obj description];
|
||||
}
|
||||
}
|
||||
|
||||
void EncodeEntitlements(::pbv1::Execution *pb_exec, SNTCachedDecision *cd) {
|
||||
::pbv1::EntitlementInfo *pb_entitlement_info = pb_exec->mutable_entitlement_info();
|
||||
|
||||
pb_entitlement_info->set_entitlements_filtered(cd.entitlementsFiltered != NO);
|
||||
|
||||
if (!cd.entitlements) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Since nested objects with varying types is hard for the API to serialize to
|
||||
// JSON, first go through and standardize types to ensure better serialization
|
||||
// as well as a consitent view of data.
|
||||
NSDictionary *entitlements = StandardizedNestedObjects(cd.entitlements, kMaxEncodeObjectLevels);
|
||||
|
||||
__block int numObjectsToEncode = (int)std::min(kMaxEncodeObjectEntries, entitlements.count);
|
||||
|
||||
pb_entitlement_info->mutable_entitlements()->Reserve(numObjectsToEncode);
|
||||
|
||||
[entitlements enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
|
||||
if (numObjectsToEncode-- == 0) {
|
||||
// Because entitlements are being clipped, ensure that we update that
|
||||
// the set of entitlements were filtered.
|
||||
pb_entitlement_info->set_entitlements_filtered(true);
|
||||
*stop = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
if (![key isKindOfClass:[NSString class]]) {
|
||||
LOGW(@"Skipping entitlement key with unexpected key type: %@", key);
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *err;
|
||||
NSData *jsonData;
|
||||
@try {
|
||||
jsonData = [NSJSONSerialization dataWithJSONObject:obj
|
||||
options:NSJSONWritingFragmentsAllowed
|
||||
error:&err];
|
||||
} @catch (NSException *e) {
|
||||
LOGW(@"Encountered entitlement that cannot directly convert to JSON: %@: %@", key, obj);
|
||||
}
|
||||
|
||||
if (!jsonData) {
|
||||
// If the first attempt to serialize to JSON failed, get a string
|
||||
// representation of the object via the `description` method and attempt
|
||||
// to serialize that instead. Serialization can fail for a number of
|
||||
// reasons, such as arrays including invalid types.
|
||||
@try {
|
||||
jsonData = [NSJSONSerialization dataWithJSONObject:[obj description]
|
||||
options:NSJSONWritingFragmentsAllowed
|
||||
error:&err];
|
||||
} @catch (NSException *e) {
|
||||
LOGW(@"Unable to create fallback string: %@: %@", key, obj);
|
||||
}
|
||||
|
||||
if (!jsonData) {
|
||||
@try {
|
||||
// As a final fallback, simply serialize an error message so that the
|
||||
// entitlement key is still logged.
|
||||
jsonData = [NSJSONSerialization dataWithJSONObject:@"JSON Serialization Failed"
|
||||
options:NSJSONWritingFragmentsAllowed
|
||||
error:&err];
|
||||
} @catch (NSException *e) {
|
||||
// This shouldn't be able to happen...
|
||||
LOGW(@"Failed to serialize fallback error message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This shouldn't be possible given the fallback code above. But handle it
|
||||
// just in case to prevent a crash.
|
||||
if (!jsonData) {
|
||||
LOGW(@"Failed to create valid JSON for entitlement: %@", key);
|
||||
return;
|
||||
}
|
||||
|
||||
::pbv1::Entitlement *pb_entitlement = pb_entitlement_info->add_entitlements();
|
||||
EncodeString([pb_entitlement] { return pb_entitlement->mutable_key(); },
|
||||
NSStringToUTF8StringView(key));
|
||||
EncodeString([pb_entitlement] { return pb_entitlement->mutable_value(); },
|
||||
NSStringToUTF8StringView([[NSString alloc] initWithData:jsonData
|
||||
encoding:NSUTF8StringEncoding]));
|
||||
}];
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCachedDecision *cd) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
@@ -524,6 +646,8 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCach
|
||||
NSString *orig_path = Utilities::OriginalPathForTranslocation(msg.es_msg().event.exec.target);
|
||||
EncodeString([pb_exec] { return pb_exec->mutable_original_path(); }, orig_path);
|
||||
|
||||
EncodeEntitlements(pb_exec, cd);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
@@ -600,6 +724,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,
|
||||
|
||||
@@ -60,6 +60,7 @@ namespace pbv1 = ::santa::pb::v1;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
extern void EncodeExitStatus(::pbv1::Exit *pbExit, int exitStatus);
|
||||
extern void EncodeEntitlements(::pbv1::Execution *pb_exec, SNTCachedDecision *cd);
|
||||
extern ::pbv1::Execution::Decision GetDecisionEnum(SNTEventState event_state);
|
||||
extern ::pbv1::Execution::Reason GetReasonEnum(SNTEventState event_state);
|
||||
extern ::pbv1::Execution::Mode GetModeEnum(SNTClientMode mode);
|
||||
@@ -68,6 +69,7 @@ extern ::pbv1::FileAccess::AccessType GetAccessType(es_event_type_t event_type);
|
||||
extern ::pbv1::FileAccess::PolicyDecision GetPolicyDecision(FileAccessPolicyDecision decision);
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
using santa::santad::logs::endpoint_security::serializers::EncodeEntitlements;
|
||||
using santa::santad::logs::endpoint_security::serializers::EncodeExitStatus;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetAccessType;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetDecisionEnum;
|
||||
@@ -110,6 +112,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 +148,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;
|
||||
@@ -164,28 +168,35 @@ std::string ConvertMessageToJsonString(const ::pbv1::SantaMessage &santaMsg) {
|
||||
return json;
|
||||
}
|
||||
|
||||
NSDictionary *findDelta(NSDictionary *a, NSDictionary *b) {
|
||||
NSMutableDictionary *delta = NSMutableDictionary.dictionary;
|
||||
NSDictionary *FindDelta(NSDictionary *want, NSDictionary *got) {
|
||||
NSMutableDictionary *delta = [NSMutableDictionary dictionary];
|
||||
delta[@"want"] = [NSMutableDictionary dictionary];
|
||||
delta[@"got"] = [NSMutableDictionary dictionary];
|
||||
|
||||
// Find objects in a that don't exist or are different in b.
|
||||
[a enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) {
|
||||
id otherObj = b[key];
|
||||
// Find objects in `want` that don't exist or are different in `got`.
|
||||
[want enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
|
||||
id otherObj = got[key];
|
||||
|
||||
if (![obj isEqual:otherObj]) {
|
||||
delta[key] = obj;
|
||||
if (!otherObj) {
|
||||
delta[@"want"][key] = obj;
|
||||
delta[@"got"][key] = @"Key missing";
|
||||
} else if (![obj isEqual:otherObj]) {
|
||||
delta[@"want"][key] = obj;
|
||||
delta[@"got"][key] = otherObj;
|
||||
}
|
||||
}];
|
||||
|
||||
// Find objects in the other dictionary that don't exist in self
|
||||
[b enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) {
|
||||
id aObj = a[key];
|
||||
// Find objects in `got` that don't exist in `want`
|
||||
[got enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
|
||||
id aObj = want[key];
|
||||
|
||||
if (!aObj) {
|
||||
delta[key] = obj;
|
||||
delta[@"want"][key] = @"Key missing";
|
||||
delta[@"got"][key] = obj;
|
||||
}
|
||||
}];
|
||||
|
||||
return delta;
|
||||
return [delta[@"want"] count] > 0 ? delta : nil;
|
||||
}
|
||||
|
||||
void SerializeAndCheck(es_event_type_t eventType,
|
||||
@@ -266,9 +277,9 @@ void SerializeAndCheck(es_event_type_t eventType,
|
||||
error:&jsonError];
|
||||
XCTAssertNil(jsonError, @"failed to parse got data as JSON");
|
||||
|
||||
XCTAssertNil(FindDelta(wantJSONDict, gotJSONDict));
|
||||
// Note: Uncomment this line to help create testfile JSON when the assert above fails
|
||||
// XCTAssertEqualObjects([NSString stringWithUTF8String:gotData.c_str()], wantData);
|
||||
NSDictionary *delta = findDelta(wantJSONDict, gotJSONDict);
|
||||
XCTAssertEqualObjects(@{}, delta);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
@@ -336,6 +347,22 @@ void SerializeAndCheckNonESEvents(
|
||||
self.testCachedDecision.quarantineURL = @"google.com";
|
||||
self.testCachedDecision.certSHA256 = @"5678_cert_hash";
|
||||
self.testCachedDecision.decisionClientMode = SNTClientModeLockdown;
|
||||
self.testCachedDecision.entitlements = @{
|
||||
@"key_with_str_val" : @"bar",
|
||||
@"key_with_num_val" : @(1234),
|
||||
@"key_with_date_val" : [NSDate dateWithTimeIntervalSince1970:1699376402],
|
||||
@"key_with_data_val" : [@"Hello World" dataUsingEncoding:NSUTF8StringEncoding],
|
||||
@"key_with_arr_val" : @[ @"v1", @"v2", @"v3" ],
|
||||
@"key_with_arr_val_nested" : @[ @"v1", @"v2", @"v3", @[ @"nv1", @"nv2" ] ],
|
||||
@"key_with_arr_val_multitype" :
|
||||
@[ @"v1", @"v2", @"v3", @(123), [NSDate dateWithTimeIntervalSince1970:1699376402] ],
|
||||
@"key_with_dict_val" : @{@"k1" : @"v1", @"k2" : @"v2"},
|
||||
@"key_with_dict_val_nested" : @{
|
||||
@"k1" : @"v1",
|
||||
@"k2" : @"v2",
|
||||
@"k3" : @{@"nk1" : @"nv1", @"nk2" : [NSDate dateWithTimeIntervalSince1970:1699376402]}
|
||||
},
|
||||
};
|
||||
|
||||
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
|
||||
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
|
||||
@@ -571,6 +598,70 @@ void SerializeAndCheckNonESEvents(
|
||||
json:YES];
|
||||
}
|
||||
|
||||
- (void)testEncodeEntitlements {
|
||||
int kMaxEncodeObjectEntries = 64; // From Protobuf.mm
|
||||
// Test basic encoding without filtered entitlements
|
||||
{
|
||||
::pbv1::Execution pbExec;
|
||||
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.entitlements = @{@"com.google.test" : @(YES)};
|
||||
|
||||
XCTAssertEqual(0, pbExec.entitlement_info().entitlements_size());
|
||||
XCTAssertFalse(cd.entitlementsFiltered);
|
||||
XCTAssertEqual(1, cd.entitlements.count);
|
||||
|
||||
EncodeEntitlements(&pbExec, cd);
|
||||
|
||||
XCTAssertEqual(1, pbExec.entitlement_info().entitlements_size());
|
||||
XCTAssertTrue(pbExec.entitlement_info().has_entitlements_filtered());
|
||||
XCTAssertFalse(pbExec.entitlement_info().entitlements_filtered());
|
||||
}
|
||||
|
||||
// Test basic encoding with filtered entitlements
|
||||
{
|
||||
::pbv1::Execution pbExec;
|
||||
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.entitlements = @{@"com.google.test" : @(YES), @"com.google.test2" : @(NO)};
|
||||
cd.entitlementsFiltered = YES;
|
||||
|
||||
XCTAssertEqual(0, pbExec.entitlement_info().entitlements_size());
|
||||
XCTAssertTrue(cd.entitlementsFiltered);
|
||||
XCTAssertEqual(2, cd.entitlements.count);
|
||||
|
||||
EncodeEntitlements(&pbExec, cd);
|
||||
|
||||
XCTAssertEqual(2, pbExec.entitlement_info().entitlements_size());
|
||||
XCTAssertTrue(pbExec.entitlement_info().has_entitlements_filtered());
|
||||
XCTAssertTrue(pbExec.entitlement_info().entitlements_filtered());
|
||||
}
|
||||
|
||||
// Test max number of entitlements logged
|
||||
// When entitlements are clipped, `entitlements_filtered` is set to true
|
||||
{
|
||||
::pbv1::Execution pbExec;
|
||||
NSMutableDictionary *ents = [NSMutableDictionary dictionary];
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ents[[NSString stringWithFormat:@"k%d", i]] = @(i);
|
||||
}
|
||||
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.entitlements = ents;
|
||||
|
||||
XCTAssertEqual(0, pbExec.entitlement_info().entitlements_size());
|
||||
XCTAssertFalse(cd.entitlementsFiltered);
|
||||
XCTAssertGreaterThan(cd.entitlements.count, kMaxEncodeObjectEntries);
|
||||
|
||||
EncodeEntitlements(&pbExec, cd);
|
||||
|
||||
XCTAssertEqual(kMaxEncodeObjectEntries, pbExec.entitlement_info().entitlements_size());
|
||||
XCTAssertTrue(pbExec.entitlement_info().has_entitlements_filtered());
|
||||
XCTAssertTrue(pbExec.entitlement_info().entitlements_filtered());
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExit {
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_EXIT
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
@@ -668,6 +759,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"
|
||||
|
||||
@@ -132,8 +132,8 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
NSError *error = nil;
|
||||
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithEndpointSecurityFile:targetFile error:&error];
|
||||
if (error) {
|
||||
LOGD(@"Unable to create SNTFileInfo while attempting to create transitive rule. Path: %@",
|
||||
@(targetFile->path.data));
|
||||
LOGD(@"Unable to create SNTFileInfo while attempting to create transitive rule. Event: %d | Path: %@ | Error: %@",
|
||||
(int)esMsg->event_type, @(targetFile->path.data), error);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -258,10 +258,19 @@ double watchdogRAMPeak = 0;
|
||||
reply();
|
||||
}
|
||||
|
||||
- (void)blockUSBMount:(void (^)(BOOL))reply {
|
||||
reply([[SNTConfigurator configurator] blockUSBMount]);
|
||||
}
|
||||
|
||||
- (void)setBlockUSBMount:(BOOL)enabled reply:(void (^)(void))reply {
|
||||
[[SNTConfigurator configurator] setBlockUSBMount:enabled];
|
||||
reply();
|
||||
}
|
||||
|
||||
- (void)remountUSBMode:(void (^)(NSArray<NSString *> *))reply {
|
||||
reply([[SNTConfigurator configurator] remountUSBMode]);
|
||||
}
|
||||
|
||||
- (void)setRemountUSBMode:(NSArray *)remountUSBMode reply:(void (^)(void))reply {
|
||||
[[SNTConfigurator configurator] setRemountUSBMode:remountUSBMode];
|
||||
reply();
|
||||
|
||||
@@ -57,7 +57,9 @@ const static NSString *kBlockLongPath = @"BlockLongPath";
|
||||
eventTable:(SNTEventTable *)eventTable
|
||||
notifierQueue:(SNTNotificationQueue *)notifierQueue
|
||||
syncdQueue:(SNTSyncdQueue *)syncdQueue
|
||||
ttyWriter:(std::shared_ptr<santa::santad::TTYWriter>)ttyWriter;
|
||||
ttyWriter:(std::shared_ptr<santa::santad::TTYWriter>)ttyWriter
|
||||
entitlementsPrefixFilter:(NSArray<NSString *> *)prefixFilter
|
||||
entitlementsTeamIDFilter:(NSArray<NSString *> *)teamIDFilter;
|
||||
|
||||
///
|
||||
/// Handles the logic of deciding whether to allow the binary to run or not, sends the response to
|
||||
@@ -82,4 +84,6 @@ const static NSString *kBlockLongPath = @"BlockLongPath";
|
||||
- (bool)synchronousShouldProcessExecEvent:
|
||||
(const santa::santad::event_providers::endpoint_security::Message &)esMsg;
|
||||
|
||||
- (void)updateEntitlementsPrefixFilter:(NSArray<NSString *> *)filter;
|
||||
- (void)updateEntitlementsTeamIDFilter:(NSArray<NSString *> *)filter;
|
||||
@end
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/SNTExecutionController.h"
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#include <bsm/libbsm.h>
|
||||
@@ -24,12 +25,16 @@
|
||||
#include <utmpx.h>
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include "Source/common/BranchPrediction.h"
|
||||
#include "Source/common/PrefixTree.h"
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTDeepCopy.h"
|
||||
#import "Source/common/SNTDropRootPrivs.h"
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
@@ -38,18 +43,39 @@
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#include "Source/common/SantaVnode.h"
|
||||
#include "Source/common/String.h"
|
||||
#include "Source/common/Unit.h"
|
||||
#import "Source/santad/DataLayer/SNTEventTable.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
#import "Source/santad/SNTNotificationQueue.h"
|
||||
#import "Source/santad/SNTPolicyProcessor.h"
|
||||
#import "Source/santad/SNTSyncdQueue.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
|
||||
using santa::common::PrefixTree;
|
||||
using santa::common::Unit;
|
||||
using santa::santad::TTYWriter;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
static const size_t kMaxAllowedPathLength = MAXPATHLEN - 1; // -1 to account for null terminator
|
||||
|
||||
void UpdateTeamIDFilterLocked(std::set<std::string> &filterSet, NSArray<NSString *> *filter) {
|
||||
filterSet.clear();
|
||||
|
||||
for (NSString *prefix in filter) {
|
||||
filterSet.insert(santa::common::NSStringToUTF8String(prefix));
|
||||
}
|
||||
}
|
||||
|
||||
void UpdatePrefixFilterLocked(std::unique_ptr<PrefixTree<Unit>> &tree,
|
||||
NSArray<NSString *> *filter) {
|
||||
tree->Reset();
|
||||
|
||||
for (NSString *item in filter) {
|
||||
tree->InsertPrefix(item.UTF8String, Unit{});
|
||||
}
|
||||
}
|
||||
|
||||
@interface SNTExecutionController ()
|
||||
@property SNTEventTable *eventTable;
|
||||
@property SNTNotificationQueue *notifierQueue;
|
||||
@@ -63,6 +89,9 @@ static const size_t kMaxAllowedPathLength = MAXPATHLEN - 1; // -1 to account fo
|
||||
|
||||
@implementation SNTExecutionController {
|
||||
std::shared_ptr<TTYWriter> _ttyWriter;
|
||||
absl::Mutex _entitlementFilterMutex;
|
||||
std::set<std::string> _entitlementsTeamIDFilter;
|
||||
std::unique_ptr<PrefixTree<Unit>> _entitlementsPrefixFilter;
|
||||
}
|
||||
|
||||
static NSString *const kPrinterProxyPreMonterey =
|
||||
@@ -79,7 +108,9 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
eventTable:(SNTEventTable *)eventTable
|
||||
notifierQueue:(SNTNotificationQueue *)notifierQueue
|
||||
syncdQueue:(SNTSyncdQueue *)syncdQueue
|
||||
ttyWriter:(std::shared_ptr<TTYWriter>)ttyWriter {
|
||||
ttyWriter:(std::shared_ptr<TTYWriter>)ttyWriter
|
||||
entitlementsPrefixFilter:(NSArray<NSString *> *)entitlementsPrefixFilter
|
||||
entitlementsTeamIDFilter:(NSArray<NSString *> *)entitlementsTeamIDFilter {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_ruleTable = ruleTable;
|
||||
@@ -100,10 +131,25 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
_events = [metricSet counterWithName:@"/santa/events"
|
||||
fieldNames:@[ @"action_response" ]
|
||||
helpText:@"Events processed by Santa per response"];
|
||||
|
||||
self->_entitlementsPrefixFilter = std::make_unique<PrefixTree<Unit>>();
|
||||
|
||||
UpdatePrefixFilterLocked(self->_entitlementsPrefixFilter, entitlementsPrefixFilter);
|
||||
UpdateTeamIDFilterLocked(self->_entitlementsTeamIDFilter, entitlementsTeamIDFilter);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)updateEntitlementsPrefixFilter:(NSArray<NSString *> *)filter {
|
||||
absl::MutexLock lock(&self->_entitlementFilterMutex);
|
||||
UpdatePrefixFilterLocked(self->_entitlementsPrefixFilter, filter);
|
||||
}
|
||||
|
||||
- (void)updateEntitlementsTeamIDFilter:(NSArray<NSString *> *)filter {
|
||||
absl::MutexLock lock(&self->_entitlementFilterMutex);
|
||||
UpdateTeamIDFilterLocked(self->_entitlementsTeamIDFilter, filter);
|
||||
}
|
||||
|
||||
- (void)incrementEventCounters:(SNTEventState)eventType {
|
||||
const NSString *eventTypeStr;
|
||||
|
||||
@@ -208,8 +254,41 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
// TODO(markowsky): Maybe add a metric here for how many large executables we're seeing.
|
||||
// if (binInfo.fileSize > SomeUpperLimit) ...
|
||||
|
||||
SNTCachedDecision *cd = [self.policyProcessor decisionForFileInfo:binInfo
|
||||
targetProcess:targetProc];
|
||||
SNTCachedDecision *cd = [self.policyProcessor
|
||||
decisionForFileInfo:binInfo
|
||||
targetProcess:targetProc
|
||||
entitlementsFilterCallback:^NSDictionary *(const char *teamID, NSDictionary *entitlements) {
|
||||
if (!entitlements) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
absl::ReaderMutexLock lock(&self->_entitlementFilterMutex);
|
||||
|
||||
if (teamID && self->_entitlementsTeamIDFilter.count(std::string(teamID)) > 0) {
|
||||
// Dropping entitlement logging for configured TeamID
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (self->_entitlementsPrefixFilter->NodeCount() == 0) {
|
||||
// Copying full entitlements for TeamID
|
||||
return [entitlements sntDeepCopy];
|
||||
} else {
|
||||
// Filtering entitlements for TeamID
|
||||
NSMutableDictionary *filtered = [NSMutableDictionary dictionary];
|
||||
|
||||
[entitlements enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
|
||||
if (!self->_entitlementsPrefixFilter->HasPrefix(key.UTF8String)) {
|
||||
if ([obj isKindOfClass:[NSArray class]] || [obj isKindOfClass:[NSDictionary class]]) {
|
||||
[filtered setObject:[obj sntDeepCopy] forKey:key];
|
||||
} else {
|
||||
[filtered setObject:[obj copy] forKey:key];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
return filtered.count > 0 ? filtered : nil;
|
||||
}
|
||||
}];
|
||||
|
||||
cd.vnodeId = SantaVnode::VnodeForFile(targetProc->executable);
|
||||
|
||||
|
||||
@@ -96,7 +96,9 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
eventTable:self.mockEventDatabase
|
||||
notifierQueue:nil
|
||||
syncdQueue:nil
|
||||
ttyWriter:nullptr];
|
||||
ttyWriter:nullptr
|
||||
entitlementsPrefixFilter:nil
|
||||
entitlementsTeamIDFilter:nil];
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
|
||||
@@ -36,22 +36,19 @@
|
||||
- (nullable instancetype)initWithRuleTable:(nonnull SNTRuleTable *)ruleTable;
|
||||
|
||||
///
|
||||
/// @param fileInfo A SNTFileInfo object.
|
||||
/// @param fileSHA256 The pre-calculated SHA256 hash for the file, can be nil. If nil the hash will
|
||||
/// be calculated by this method from the filePath.
|
||||
/// @param certificateSHA256 The pre-calculated SHA256 hash of the leaf certificate. If nil, the
|
||||
/// signature will be validated on the binary represented by fileInfo.
|
||||
///
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID;
|
||||
|
||||
/// Convenience initializer. Will obtain the teamID and construct the signingID
|
||||
/// identifier if able.
|
||||
///
|
||||
/// IMPORTANT: The lifetimes of arguments to `entitlementsFilterCallback` are
|
||||
/// only guaranteed for the duration of the call to the block. Do not perform
|
||||
/// any async processing without extending their lifetimes.
|
||||
///
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
targetProcess:(nonnull const es_process_t *)targetProc;
|
||||
targetProcess:(nonnull const es_process_t *)targetProc
|
||||
entitlementsFilterCallback:
|
||||
(NSDictionary *_Nullable (^_Nonnull)(
|
||||
const char *_Nullable teamID,
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback;
|
||||
|
||||
///
|
||||
/// A wrapper for decisionForFileInfo:fileSHA256:certificateSHA256:. This method is slower as it
|
||||
|
||||
@@ -13,15 +13,18 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/SNTPolicyProcessor.h"
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#import <Security/SecCode.h>
|
||||
|
||||
#include "Source/common/SNTLogging.h"
|
||||
#import <Security/Security.h>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTDeepCopy.h"
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
|
||||
@@ -45,7 +48,11 @@
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID {
|
||||
signingID:(nullable NSString *)signingID
|
||||
isProdSignedCallback:(BOOL (^_Nonnull)())isProdSignedCallback
|
||||
entitlementsFilterCallback:
|
||||
(NSDictionary *_Nullable (^_Nullable)(
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.sha256 = fileSHA256 ?: fileInfo.SHA256;
|
||||
cd.teamID = teamID;
|
||||
@@ -92,10 +99,34 @@
|
||||
cd.signingID = nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSDictionary *entitlements =
|
||||
csInfo.signingInformation[(__bridge NSString *)kSecCodeInfoEntitlementsDict];
|
||||
|
||||
if (entitlementsFilterCallback) {
|
||||
cd.entitlements = entitlementsFilterCallback(entitlements);
|
||||
cd.entitlementsFiltered = (cd.entitlements.count == entitlements.count);
|
||||
} else {
|
||||
cd.entitlements = [entitlements sntDeepCopy];
|
||||
cd.entitlementsFiltered = NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
cd.quarantineURL = fileInfo.quarantineDataURL;
|
||||
|
||||
// Do not evaluate TeamID/SigningID rules for dev-signed code based on the
|
||||
// assumption that orgs are generally more relaxed about dev signed cert
|
||||
// protections and users can more easily produce dev-signed code that
|
||||
// would otherwise be inadvertently allowed.
|
||||
// Note: Only perform the check if the SigningID is still set, otherwise
|
||||
// it is unsigned or had issues above that already cleared the values.
|
||||
if (cd.signingID && !isProdSignedCallback()) {
|
||||
LOGD(@"Ignoring TeamID and SigningID rules for code not signed with production cert: %@",
|
||||
cd.signingID);
|
||||
cd.teamID = nil;
|
||||
cd.signingID = nil;
|
||||
}
|
||||
|
||||
SNTRule *rule = [self.ruleTable ruleForBinarySHA256:cd.sha256
|
||||
signingID:cd.signingID
|
||||
certificateSHA256:cd.certSHA256
|
||||
@@ -221,17 +252,25 @@
|
||||
}
|
||||
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
targetProcess:(nonnull const es_process_t *)targetProc {
|
||||
targetProcess:(nonnull const es_process_t *)targetProc
|
||||
entitlementsFilterCallback:
|
||||
(NSDictionary *_Nullable (^_Nonnull)(
|
||||
const char *_Nullable teamID,
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
|
||||
NSString *signingID;
|
||||
NSString *teamID;
|
||||
|
||||
const char *entitlementsFilterTeamID = NULL;
|
||||
|
||||
if (targetProc->signing_id.length > 0) {
|
||||
if (targetProc->team_id.length > 0) {
|
||||
entitlementsFilterTeamID = targetProc->team_id.data;
|
||||
teamID = [NSString stringWithUTF8String:targetProc->team_id.data];
|
||||
signingID =
|
||||
[NSString stringWithFormat:@"%@:%@", teamID,
|
||||
[NSString stringWithUTF8String:targetProc->signing_id.data]];
|
||||
} else if (targetProc->is_platform_binary) {
|
||||
entitlementsFilterTeamID = "platform";
|
||||
signingID =
|
||||
[NSString stringWithFormat:@"platform:%@",
|
||||
[NSString stringWithUTF8String:targetProc->signing_id.data]];
|
||||
@@ -239,10 +278,16 @@
|
||||
}
|
||||
|
||||
return [self decisionForFileInfo:fileInfo
|
||||
fileSHA256:nil
|
||||
certificateSHA256:nil
|
||||
teamID:teamID
|
||||
signingID:signingID];
|
||||
fileSHA256:nil
|
||||
certificateSHA256:nil
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
isProdSignedCallback:^BOOL {
|
||||
return ((targetProc->codesigning_flags & CS_DEV_CODE) == 0);
|
||||
}
|
||||
entitlementsFilterCallback:^NSDictionary *(NSDictionary *entitlements) {
|
||||
return entitlementsFilterCallback(entitlementsFilterTeamID, entitlements);
|
||||
}];
|
||||
}
|
||||
|
||||
// Used by `$ santactl fileinfo`.
|
||||
@@ -251,15 +296,37 @@
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID {
|
||||
SNTFileInfo *fileInfo;
|
||||
MOLCodesignChecker *csInfo;
|
||||
NSError *error;
|
||||
fileInfo = [[SNTFileInfo alloc] initWithPath:filePath error:&error];
|
||||
if (!fileInfo) LOGW(@"Failed to read file %@: %@", filePath, error.localizedDescription);
|
||||
|
||||
SNTFileInfo *fileInfo = [[SNTFileInfo alloc] initWithPath:filePath error:&error];
|
||||
if (!fileInfo) {
|
||||
LOGW(@"Failed to read file %@: %@", filePath, error.localizedDescription);
|
||||
} else {
|
||||
csInfo = [fileInfo codesignCheckerWithError:&error];
|
||||
if (error) {
|
||||
LOGW(@"Failed to get codesign ingo for file %@: %@", filePath, error.localizedDescription);
|
||||
}
|
||||
}
|
||||
|
||||
return [self decisionForFileInfo:fileInfo
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
signingID:signingID];
|
||||
signingID:signingID
|
||||
isProdSignedCallback:^BOOL {
|
||||
if (csInfo) {
|
||||
// Development OID values defined by Apple and used by the Security Framework
|
||||
// https://images.apple.com/certificateauthority/pdf/Apple_WWDR_CPS_v1.31.pdf
|
||||
NSArray *keys = @[ @"1.2.840.113635.100.6.1.2", @"1.2.840.113635.100.6.1.12" ];
|
||||
NSDictionary *vals = CFBridgingRelease(SecCertificateCopyValues(
|
||||
csInfo.leafCertificate.certRef, (__bridge CFArrayRef)keys, NULL));
|
||||
return vals.count == 0;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
entitlementsFilterCallback:nil];
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
@@ -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
|
||||
@@ -357,6 +358,52 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
|
||||
// Forcefully exit. The daemon will be restarted immediately.
|
||||
exit(EXIT_SUCCESS);
|
||||
}],
|
||||
[[SNTKVOManager alloc]
|
||||
initWithObject:configurator
|
||||
selector:@selector(entitlementsTeamIDFilter)
|
||||
type:[NSArray class]
|
||||
callback:^(NSArray<NSString *> *oldValue, NSArray<NSString *> *newValue) {
|
||||
if ((!oldValue && !newValue) || [oldValue isEqualToArray:newValue]) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGI(@"EntitlementsTeamIDFilter changed. '%@' --> '%@'. Flushing caches.", oldValue,
|
||||
newValue);
|
||||
|
||||
// Get the value from the configurator since that method ensures proper structure
|
||||
[exec_controller
|
||||
updateEntitlementsTeamIDFilter:[configurator entitlementsTeamIDFilter]];
|
||||
|
||||
// Clear the AuthResultCache, then clear the ES cache to ensure
|
||||
// future execs get SNTCachedDecision entitlement values filtered
|
||||
// with the new settings.
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches,
|
||||
FlushCacheReason::kEntitlementsTeamIDFilterChanged);
|
||||
[authorizer_client clearCache];
|
||||
}],
|
||||
[[SNTKVOManager alloc]
|
||||
initWithObject:configurator
|
||||
selector:@selector(entitlementsPrefixFilter)
|
||||
type:[NSArray class]
|
||||
callback:^(NSArray<NSString *> *oldValue, NSArray<NSString *> *newValue) {
|
||||
if ((!oldValue && !newValue) || [oldValue isEqualToArray:newValue]) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGI(@"EntitlementsPrefixFilter changed. '%@' --> '%@'. Flushing caches.", oldValue,
|
||||
newValue);
|
||||
|
||||
// Get the value from the configurator since that method ensures proper structure
|
||||
[exec_controller
|
||||
updateEntitlementsPrefixFilter:[configurator entitlementsPrefixFilter]];
|
||||
|
||||
// Clear the AuthResultCache, then clear the ES cache to ensure
|
||||
// future execs get SNTCachedDecision entitlement values filtered
|
||||
// with the new settings.
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches,
|
||||
FlushCacheReason::kEntitlementsPrefixFilterChanged);
|
||||
[authorizer_client clearCache];
|
||||
}],
|
||||
]];
|
||||
|
||||
if (@available(macOS 13.0, *)) {
|
||||
|
||||
@@ -94,7 +94,9 @@ std::unique_ptr<SantadDeps> SantadDeps::Create(SNTConfigurator *configurator,
|
||||
eventTable:event_table
|
||||
notifierQueue:notifier_queue
|
||||
syncdQueue:syncd_queue
|
||||
ttyWriter:tty_writer];
|
||||
ttyWriter:tty_writer
|
||||
entitlementsPrefixFilter:[configurator entitlementsPrefixFilter]
|
||||
entitlementsTeamIDFilter:[configurator entitlementsTeamIDFilter]];
|
||||
if (!exec_controller) {
|
||||
LOGE(@"Failed to initialize exec controller.");
|
||||
exit(EXIT_FAILURE);
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Source/santad/testdata/protobuf/v1/exec.json
vendored
43
Source/santad/testdata/protobuf/v1/exec.json
vendored
@@ -120,5 +120,46 @@
|
||||
}
|
||||
},
|
||||
"explain": "extra!",
|
||||
"quarantine_url": "google.com"
|
||||
"quarantine_url": "google.com",
|
||||
"entitlement_info": {
|
||||
"entitlements_filtered": false,
|
||||
"entitlements": [
|
||||
{
|
||||
"key": "key_with_arr_val_multitype",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",123,\"2023-11-07T17:00:02.000Z\"]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_data_val",
|
||||
"value": "\"SGVsbG8gV29ybGQ=\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_str_val",
|
||||
"value": "\"bar\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val_nested",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",[\"nv1\",\"nv2\"]]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val",
|
||||
"value": "{\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_date_val",
|
||||
"value": "\"2023-11-07T17:00:02.000Z\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val_nested",
|
||||
"value": "{\"k3\":{\"nk1\":\"nv1\",\"nk2\":\"2023-11-07T17:00:02.000Z\"},\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_num_val",
|
||||
"value": "1234"
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val",
|
||||
"value": "[\"v1\",\"v2\",\"v3\"]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Source/santad/testdata/protobuf/v2/exec.json
vendored
43
Source/santad/testdata/protobuf/v2/exec.json
vendored
@@ -148,5 +148,46 @@
|
||||
}
|
||||
},
|
||||
"explain": "extra!",
|
||||
"quarantine_url": "google.com"
|
||||
"quarantine_url": "google.com",
|
||||
"entitlement_info": {
|
||||
"entitlements_filtered": false,
|
||||
"entitlements": [
|
||||
{
|
||||
"key": "key_with_arr_val_multitype",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",123,\"2023-11-07T17:00:02.000Z\"]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_data_val",
|
||||
"value": "\"SGVsbG8gV29ybGQ=\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_str_val",
|
||||
"value": "\"bar\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val_nested",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",[\"nv1\",\"nv2\"]]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val",
|
||||
"value": "{\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_date_val",
|
||||
"value": "\"2023-11-07T17:00:02.000Z\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val_nested",
|
||||
"value": "{\"k3\":{\"nk1\":\"nv1\",\"nk2\":\"2023-11-07T17:00:02.000Z\"},\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_num_val",
|
||||
"value": "1234"
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val",
|
||||
"value": "[\"v1\",\"v2\",\"v3\"]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Source/santad/testdata/protobuf/v4/exec.json
vendored
43
Source/santad/testdata/protobuf/v4/exec.json
vendored
@@ -197,5 +197,46 @@
|
||||
}
|
||||
},
|
||||
"explain": "extra!",
|
||||
"quarantine_url": "google.com"
|
||||
"quarantine_url": "google.com",
|
||||
"entitlement_info": {
|
||||
"entitlements_filtered": false,
|
||||
"entitlements": [
|
||||
{
|
||||
"key": "key_with_arr_val_multitype",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",123,\"2023-11-07T17:00:02.000Z\"]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_data_val",
|
||||
"value": "\"SGVsbG8gV29ybGQ=\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_str_val",
|
||||
"value": "\"bar\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val_nested",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",[\"nv1\",\"nv2\"]]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val",
|
||||
"value": "{\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_date_val",
|
||||
"value": "\"2023-11-07T17:00:02.000Z\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val_nested",
|
||||
"value": "{\"k3\":{\"nk1\":\"nv1\",\"nk2\":\"2023-11-07T17:00:02.000Z\"},\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_num_val",
|
||||
"value": "1234"
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val",
|
||||
"value": "[\"v1\",\"v2\",\"v3\"]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Source/santad/testdata/protobuf/v5/exec.json
vendored
43
Source/santad/testdata/protobuf/v5/exec.json
vendored
@@ -197,5 +197,46 @@
|
||||
}
|
||||
},
|
||||
"explain": "extra!",
|
||||
"quarantine_url": "google.com"
|
||||
"quarantine_url": "google.com",
|
||||
"entitlement_info": {
|
||||
"entitlements_filtered": false,
|
||||
"entitlements": [
|
||||
{
|
||||
"key": "key_with_arr_val_multitype",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",123,\"2023-11-07T17:00:02.000Z\"]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_data_val",
|
||||
"value": "\"SGVsbG8gV29ybGQ=\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_str_val",
|
||||
"value": "\"bar\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val_nested",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",[\"nv1\",\"nv2\"]]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val",
|
||||
"value": "{\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_date_val",
|
||||
"value": "\"2023-11-07T17:00:02.000Z\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val_nested",
|
||||
"value": "{\"k3\":{\"nk1\":\"nv1\",\"nk2\":\"2023-11-07T17:00:02.000Z\"},\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_num_val",
|
||||
"value": "1234"
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val",
|
||||
"value": "[\"v1\",\"v2\",\"v3\"]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Source/santad/testdata/protobuf/v6/exec.json
vendored
43
Source/santad/testdata/protobuf/v6/exec.json
vendored
@@ -197,5 +197,46 @@
|
||||
}
|
||||
},
|
||||
"explain": "extra!",
|
||||
"quarantine_url": "google.com"
|
||||
"quarantine_url": "google.com",
|
||||
"entitlement_info": {
|
||||
"entitlements_filtered": false,
|
||||
"entitlements": [
|
||||
{
|
||||
"key": "key_with_arr_val_multitype",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",123,\"2023-11-07T17:00:02.000Z\"]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_data_val",
|
||||
"value": "\"SGVsbG8gV29ybGQ=\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_str_val",
|
||||
"value": "\"bar\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val_nested",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",[\"nv1\",\"nv2\"]]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val",
|
||||
"value": "{\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_date_val",
|
||||
"value": "\"2023-11-07T17:00:02.000Z\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val_nested",
|
||||
"value": "{\"k3\":{\"nk1\":\"nv1\",\"nk2\":\"2023-11-07T17:00:02.000Z\"},\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_num_val",
|
||||
"value": "1234"
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val",
|
||||
"value": "[\"v1\",\"v2\",\"v3\"]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,10 +66,6 @@ macos_command_line_application(
|
||||
],
|
||||
infoplists = ["Info.plist"],
|
||||
minimum_os_version = "11.0",
|
||||
provisioning_profile = select({
|
||||
"//:adhoc_build": None,
|
||||
"//conditions:default": "//profiles:santa_dev",
|
||||
}),
|
||||
version = "//:version",
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [
|
||||
|
||||
@@ -168,10 +168,6 @@ macos_command_line_application(
|
||||
],
|
||||
infoplists = ["Info.plist"],
|
||||
minimum_os_version = "11.0",
|
||||
provisioning_profile = select({
|
||||
"//:adhoc_build": None,
|
||||
"//conditions:default": "//profiles:santa_dev",
|
||||
}),
|
||||
version = "//:version",
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [":santass_lib"],
|
||||
|
||||
@@ -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,17 @@ 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. |
|
||||
| EntitlementsPrefixFilter | Array | Array of strings of entitlement prefixes that should not be logged (for example: `com.apple.private`). No default. |
|
||||
| EntitlementsTeamIDFilter | Array | Array of TeamID strings. Entitlements from processes with a matching TeamID in the code signature will not be logged. Use the value `platform` to filter entitlements from platform binaries. No default. |
|
||||
|
||||
|
||||
\*overridable by the sync server: run `santactl status` to check the current
|
||||
@@ -223,9 +227,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 +241,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