Compare commits

...

33 Commits

Author SHA1 Message Date
Matt W
221664436f Expand debug logging for transitive rule failure case (#1248) 2023-11-30 15:47:48 -05:00
Russell Hancox
65c660298c Project: Remove provisioning_profiles attributes from command-line tool rules (#1247) 2023-11-30 13:50:38 -05:00
Matt W
2b5d55781c Revert back to C++17 for now (#1246) 2023-11-29 21:39:48 -05:00
Matt W
84e6d6ccff Fix USB state issue in santactl status (#1244) 2023-11-29 17:56:35 -05:00
Matt W
c16f90f5f9 Fix test issue caused by move to C++20 (#1245)
* Fix test issue caused by move to C++20

* Use spaceship operator as is the style of the time

* lint

* Add include
2023-11-29 16:52:23 -05:00
Matt W
d503eae4d9 Bump to C++20 (#1243) 2023-11-29 09:57:45 -05:00
Matt W
818518bb38 Ignore TeamID and SigningID rules for dev signed code (#1241)
* Ignore TID/SID rules for dev signed code

* Handle code paths from santactl

* Don't bother evaluating isProdSignedCallback if not necessary

* PR feedback. Link to docs.
2023-11-27 11:21:17 -05:00
Matt W
f499654951 Experimental metrics (#1238)
* Experimental metrics

* Fix tests, old platform builds

* Use more recent availability checks

* Update macro name, undef after usage
2023-11-20 13:02:58 -05:00
Matt W
a5e8d77d06 Entitlements logging config options (#1233)
* WIP add config support to filter logged entitlements

* Add EntitlementInfo proto message to store if entitlements were filtered

* Log cleanup

* Address PR feedback

* Address PR feedback
2023-11-13 09:39:32 -05:00
Matt W
edac42e8b8 Fix internal build issues, minor cleanup. (#1231) 2023-11-09 17:26:31 -05:00
Matt W
ce5e3d0ee4 Add support for logging entitlements in EXEC events (#1225)
* Add support for logging entitlements in EXEC events

* Standardize entitlement dictionary formatting
2023-11-09 16:26:57 -05:00
Pete Markowsky
3e51ec6b8a Add name for white space check (#1223)
* Add a name to the whitespace check in the check-markdown workflow.

* Pin workflow steps.
2023-11-09 15:26:51 -05:00
Travis Lane
ed227f43d4 Explicitly cast strings to std::string_view (#1230)
GoogleTest when built with GTEST_HAS_ABSL fails to convert these strings
to a `std::string_view`. Lets instead explicitly convert them to a
`std::string_view`.
2023-11-08 17:05:08 -05:00
Nick Gregory
056ed75bf1 dismiss santa popup after integration tests (#1226) 2023-11-07 14:42:03 -05:00
Matt W
8f5f8de245 Only remount on startup if remount args are set (#1222) 2023-11-06 09:10:34 -05:00
Matt W
7c58648c35 Bump hedron commit. Minor WORKSPACE fixups. (#1221) 2023-11-03 10:03:11 -04:00
Matt W
3f3751eb18 Fix remount issue for APFS formatted drives (#1220)
* Fix issue mounting APFS drives with MNT_JOURNALED

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

* Update help text

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

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

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

* no poweroff

* no start

* drive usb via sync server since its up

sudo santactl status

sudo?

* revert nostart/nopoweroff

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

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

* WIP fixup existing tests

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

* no shutdown

* gh path

* dismiss santa popup after bad binary

* sleep for ui

* re-enable start vm

* re-enable poweroff

* tabs

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

* Removed unused --json option from help string.

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

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

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

View File

@@ -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'"

View File

@@ -1,25 +1,28 @@
name: E2E
on: workflow_dispatch
on:
schedule:
- cron: '0 4 * * *' # Every day at 4:00 UTC (not to interfere with fuzzing)
workflow_dispatch:
jobs:
start_vm:
runs-on: e2e-host
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
- name: Start VM
run: python3 Testing/integration/actions/start_vm.py macOS_12.bundle.tar.gz
run: python3 Testing/integration/actions/start_vm.py macOS_14.bundle.tar.gz
integration:
runs-on: e2e-vm
env:
VM_PASSWORD: ${{ secrets.VM_PASSWORD }}
steps:
- uses: actions/checkout@v3
- name: Install configuration profile
run: bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
- name: Add homebrew to PATH
run: echo "/opt/homebrew/bin/" >> $GITHUB_PATH
- name: Install configuration profile
run: bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig
- name: Build, install, and start moroz
run: |
bazel build @com_github_groob_moroz//cmd/moroz:moroz
@@ -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

View File

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

View File

@@ -40,6 +40,12 @@ objc_library(
],
)
objc_library(
name = "SNTDeepCopy",
srcs = ["SNTDeepCopy.m"],
hdrs = ["SNTDeepCopy.h"],
)
cc_library(
name = "SantaCache",
hdrs = ["SantaCache.h"],

View File

@@ -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:

View File

@@ -38,6 +38,8 @@
@property NSArray<MOLCertificate *> *certChain;
@property NSString *teamID;
@property NSString *signingID;
@property NSDictionary *entitlements;
@property BOOL entitlementsFiltered;
@property NSString *quarantineURL;

View File

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

View File

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

View File

@@ -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 {

View 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

View 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

View File

@@ -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

View File

@@ -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;
}
};

View File

@@ -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, *)) {

View File

@@ -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;
};
}

View File

@@ -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"],

View File

@@ -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",

View File

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

View File

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

View File

@@ -15,11 +15,22 @@
#import <Foundation/Foundation.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/santactl/SNTCommand.h"
#import "Source/santactl/SNTCommandController.h"
NSString *StartupOptionToString(SNTDeviceManagerStartupPreferences pref) {
switch (pref) {
case SNTDeviceManagerStartupPreferencesUnmount: return @"Unmount";
case SNTDeviceManagerStartupPreferencesForceUnmount: return @"ForceUnmount";
case SNTDeviceManagerStartupPreferencesRemount: return @"Remount";
case SNTDeviceManagerStartupPreferencesForceRemount: return @"ForceRemount";
default: return @"None";
}
}
@interface SNTCommandStatus : SNTCommand <SNTCommandProtocol>
@end
@@ -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);

View File

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

View File

@@ -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",

View File

@@ -41,6 +41,8 @@ enum class FlushCacheReason {
kStaticRulesChanged,
kExplicitCommand,
kFilesystemUnmounted,
kEntitlementsPrefixFilterChanged,
kEntitlementsTeamIDFilterChanged,
};
class AuthResultCache {

View File

@@ -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)];

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,6 +46,7 @@ using google::protobuf::json::MessageToJsonString;
using santa::common::NSStringToUTF8StringView;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::EnrichedClose;
using santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated;
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
using santa::santad::event_providers::endpoint_security::EnrichedExec;
@@ -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,

View File

@@ -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},

View File

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

View File

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

View File

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

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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

View File

@@ -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];
}
///

View File

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

View File

@@ -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);

View File

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

View File

@@ -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\"]"
}
]
}
}

View File

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

View File

@@ -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\"]"
}
]
}
}

View File

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

View File

@@ -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\"]"
}
]
}
}

View File

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

View File

@@ -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\"]"
}
]
}
}

View File

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

View File

@@ -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\"]"
}
]
}
}

View File

@@ -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 = [

View File

@@ -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"],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@@ -69,13 +69,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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

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

View File

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