Compare commits

...

49 Commits

Author SHA1 Message Date
Matt W
70474aba3e Sync clean all (#1275)
* WIP Clean syncs now leave non-transitive rules by default

* WIP Get existing tests compiling and passing

* Remove clean all sync server key. Basic tests.

* Add SNTConfiguratorTest, test deprecated key migration

* Revert changes to santactl status output

* Add new preflight response sync type key, lots of tests

* Rework configurator flow a bit so calls cannot be made out of order

* Comment clean sync states. Test all permutations.

* Update docs for new sync keys

* Doc updates as requested in PR
2024-01-24 09:26:20 -05:00
Pete Markowsky
f4ad76b974 Make santactl status always print out transitive rule status if set (#1277)
* Make santactl status always print out transitive rule status even when not using a sync service.

* Fix typo in SNTCommandRule.m.

* Updated JSON values to put transitive_rules in the daemon section.
2024-01-22 12:16:47 -05:00
hugo-syn
3b7061ea62 chore: Fix typo s/occured/occurred/ (#1274)
Signed-off-by: hugo-syn <hugo.vincent@synacktiv.com>
2024-01-18 10:50:01 -05:00
hugo-syn
280d93ee08 chore: Fix multiple typos (#1273)
Signed-off-by: hugo-syn <hugo.vincent@synacktiv.com>
2024-01-18 09:17:52 -05:00
Matt W
f73463117f Add back support for EnableForkAndExitLogging config key (#1271) 2024-01-14 13:42:06 -05:00
Matt W
f93e1a56a0 Docs add missing config keys (#1270)
* Add missing config keys

* Use more consistent wording

* More consistent whitespace

* Reorder constants to appropriate section groups

* Update docs/deployment/configuration.md

Co-authored-by: Pete Markowsky <pmarkowsky@users.noreply.github.com>

---------

Co-authored-by: Pete Markowsky <pmarkowsky@users.noreply.github.com>
2024-01-13 00:08:16 -05:00
Pete Markowsky
d5195b55d2 Added documentation to clarify clean sync with zero rule behavior (#1259)
* Added documentation to clarify clean sync with zero rule behaivor.

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>
2024-01-09 16:10:27 -05:00
Matt W
15e5874d43 Fix wrong srcs paths (#1265) 2024-01-03 10:49:08 -05:00
Matt W
5e6fa09f1c Change build target visibility (#1264)
* Change build target visibility

* Add dependent headers as srcs. Remove unnecessary visibility.
2024-01-03 10:21:33 -05:00
Matt W
ce2777ae94 Fix santactl rule --check (#1262)
* Fix santactl rule check to only strictly show rule info

* Reorganized to make more testable, added tests
2024-01-03 09:52:14 -05:00
Matt W
f8a20d35b4 Fix issue with drop count calculations (#1256) 2023-12-13 17:01:11 -05:00
Matt W
2e69370524 Event drop metrics (#1253)
* Add dropped event detection and metrics

* Update metrics test for drop counts

* Comment new interface
2023-12-07 15:23:51 -05:00
Russell Hancox
f9b4e00e0c GUI: Change default button text to "Open..." (#1254) 2023-12-06 14:19:27 -05:00
Matt W
e2e83a099c Initial support for some scoped types (#1250)
* Add some scoped types to handle automatic releasing

* style

* comment typo
2023-12-05 18:51:07 -05:00
Matt W
2cbf15566a Revert "Project: Remove provisioning_profiles attributes from command-line tool rules (#1247)" (#1251)
This reverts commit 65c660298c.
2023-12-05 15:48:36 -05:00
Nick Gregory
1596990c65 reorder e2e tests (#1249) 2023-12-04 13:01:30 -05:00
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
127 changed files with 3745 additions and 617 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,41 +1,49 @@
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 sync santa
run: |
bazel run :reload --define=SANTA_BUILD_TYPE=adhoc
bazel run //Testing/integration:allow_sysex
- name: Test config changes
run: ./Testing/integration/test_config_changes.sh
- name: Build, install, and start moroz
run: |
bazel build @com_github_groob_moroz//cmd/moroz:moroz
cp bazel-bin/external/com_github_groob_moroz/cmd/moroz/moroz_/moroz /tmp/moroz
/tmp/moroz -configs="$GITHUB_WORKSPACE/Testing/integration/configs/moroz_default/global.toml" -use-tls=false &
- name: Build, install, and sync santa
run: |
bazel run :reload --define=SANTA_BUILD_TYPE=adhoc
bazel run //Testing/integration:allow_sysex
sudo santactl sync --debug
- name: Run integration test binaries
run: bazel test //Testing/integration:integration_tests
- name: Test config changes
run: ./Testing/integration/test_config_changes.sh
run: |
bazel test //Testing/integration:integration_tests
sleep 3
bazel run //Testing/integration:dismiss_santa_popup || true
- 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

@@ -18,6 +18,7 @@
#import <MOLXPCConnection/MOLXPCConnection.h>
#import "SNTCommandController.h"
#import "SNTCommonEnums.h"
#import "SNTRule.h"
#import "SNTXPCControlInterface.h"
@@ -58,7 +59,7 @@ extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, std::size_t size
[daemonConn resume];
[[daemonConn remoteObjectProxy]
databaseRuleAddRules:@[ newRule ]
cleanSlate:NO
ruleCleanup:SNTRuleCleanupNone
reply:^(NSError *error) {
if (!error) {
if (newRule.state == SNTRuleStateRemove) {

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"],
@@ -54,6 +60,56 @@ santa_unit_test(
],
)
# This target shouldn't be used directly.
# Use a more specific scoped type instead.
objc_library(
name = "ScopedTypeRef",
hdrs = ["ScopedTypeRef.h"],
visibility = ["//Source/common:__pkg__"],
)
objc_library(
name = "ScopedCFTypeRef",
hdrs = ["ScopedCFTypeRef.h"],
deps = [
":ScopedTypeRef",
],
)
santa_unit_test(
name = "ScopedCFTypeRefTest",
srcs = ["ScopedCFTypeRefTest.mm"],
sdk_frameworks = [
"Security",
],
deps = [
":ScopedCFTypeRef",
],
)
objc_library(
name = "ScopedIOObjectRef",
hdrs = ["ScopedIOObjectRef.h"],
sdk_frameworks = [
"IOKit",
],
deps = [
":ScopedTypeRef",
],
)
santa_unit_test(
name = "ScopedIOObjectRefTest",
srcs = ["ScopedIOObjectRefTest.mm"],
sdk_frameworks = [
"IOKit",
],
deps = [
":ScopedIOObjectRef",
"//Source/santad:EndpointSecuritySerializerUtilities",
],
)
objc_library(
name = "BranchPrediction",
hdrs = ["BranchPrediction.h"],
@@ -421,17 +477,30 @@ santa_unit_test(
],
)
santa_unit_test(
name = "SNTConfiguratorTest",
srcs = ["SNTConfiguratorTest.m"],
deps = [
":SNTCommonEnums",
":SNTConfigurator",
"@OCMock",
],
)
test_suite(
name = "unit_tests",
tests = [
":PrefixTreeTest",
":SNTBlockMessageTest",
":SNTCachedDecisionTest",
":SNTConfiguratorTest",
":SNTFileInfoTest",
":SNTKVOManagerTest",
":SNTMetricSetTest",
":SNTRuleTest",
":SantaCacheTest",
":ScopedCFTypeRefTest",
":ScopedIOObjectRefTest",
],
visibility = ["//:santa_package_group"],
)

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,26 @@ typedef NS_ENUM(NSInteger, SNTOverrideFileAccessAction) {
SNTOverrideFileAccessActionDiable,
};
typedef NS_ENUM(NSInteger, SNTDeviceManagerStartupPreferences) {
SNTDeviceManagerStartupPreferencesNone,
SNTDeviceManagerStartupPreferencesUnmount,
SNTDeviceManagerStartupPreferencesForceUnmount,
SNTDeviceManagerStartupPreferencesRemount,
SNTDeviceManagerStartupPreferencesForceRemount,
};
typedef NS_ENUM(NSInteger, SNTSyncType) {
SNTSyncTypeNormal,
SNTSyncTypeClean,
SNTSyncTypeCleanAll,
};
typedef NS_ENUM(NSInteger, SNTRuleCleanup) {
SNTRuleCleanupNone,
SNTRuleCleanupAll,
SNTRuleCleanupNonTransitive,
};
#ifdef __cplusplus
enum class FileAccessPolicyDecision {
kNoPolicy,

View File

@@ -284,7 +284,7 @@
///
/// Enabling this appends the Santa machine ID to the end of each log line. If nothing
/// has been overriden, this is the host's UUID.
/// has been overridden, this is the host's UUID.
/// Defaults to NO.
///
@property(readonly, nonatomic) BOOL enableMachineIDDecoration;
@@ -437,9 +437,9 @@
@property(nonatomic) NSDate *ruleSyncLastSuccess;
///
/// If YES a clean sync is required.
/// Type of sync required (e.g. normal, clean, etc.).
///
@property(nonatomic) BOOL syncCleanRequired;
@property(nonatomic) SNTSyncType syncTypeRequired;
#pragma mark - USB Settings
@@ -449,11 +449,25 @@
@property(nonatomic) BOOL blockUSBMount;
///
/// Comma-seperated `$ mount -o` arguments used for forced remounting of USB devices. Default
/// Comma-separated `$ mount -o` arguments used for forced remounting of USB devices. Default
/// to fully allow/deny without remounting if unset.
///
@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;
@@ -38,6 +53,9 @@
/// Holds the last processed hash of the static rules list.
@property(atomic) NSDictionary *cachedStaticRules;
@property(readonly, nonatomic) NSString *syncStateFilePath;
@property(nonatomic, copy) BOOL (^syncStateAccessAuthorizerBlock)();
@end
@implementation SNTConfigurator
@@ -88,6 +106,8 @@ static NSString *const kModeNotificationLockdown = @"ModeNotificationLockdown";
static NSString *const kEnablePageZeroProtectionKey = @"EnablePageZeroProtection";
static NSString *const kEnableBadSignatureProtectionKey = @"EnableBadSignatureProtection";
static NSString *const kFailClosedKey = @"FailClosed";
static NSString *const kDisableUnknownEventUploadKey = @"DisableUnknownEventUpload";
static NSString *const kFileChangesRegexKey = @"FileChangesRegex";
static NSString *const kFileChangesPrefixFiltersKey = @"FileChangesPrefixFilters";
@@ -116,9 +136,19 @@ 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";
static NSString *const kOnStartUSBOptions = @"OnStartUSBOptions";
static NSString *const kMetricFormat = @"MetricFormat";
static NSString *const kMetricURL = @"MetricURL";
static NSString *const kMetricExportInterval = @"MetricExportInterval";
static NSString *const kMetricExportTimeout = @"MetricExportTimeout";
static NSString *const kMetricExtraLabels = @"MetricExtraLabels";
// 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 kEnableTransitiveRulesKey = @"EnableTransitiveRules";
@@ -128,21 +158,24 @@ static NSString *const kAllowedPathRegexKeyDeprecated = @"WhitelistRegex";
static NSString *const kBlockedPathRegexKey = @"BlockedPathRegex";
static NSString *const kBlockedPathRegexKeyDeprecated = @"BlacklistRegex";
static NSString *const kEnableAllEventUploadKey = @"EnableAllEventUpload";
static NSString *const kDisableUnknownEventUploadKey = @"DisableUnknownEventUpload";
static NSString *const kOverrideFileAccessActionKey = @"OverrideFileAccessAction";
static NSString *const kMetricFormat = @"MetricFormat";
static NSString *const kMetricURL = @"MetricURL";
static NSString *const kMetricExportInterval = @"MetricExportInterval";
static NSString *const kMetricExportTimeout = @"MetricExportTimeout";
static NSString *const kMetricExtraLabels = @"MetricExtraLabels";
// The keys managed by a sync server.
static NSString *const kFullSyncLastSuccess = @"FullSyncLastSuccess";
static NSString *const kRuleSyncLastSuccess = @"RuleSyncLastSuccess";
static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
static NSString *const kSyncCleanRequiredDeprecated = @"SyncCleanRequired";
static NSString *const kSyncTypeRequired = @"SyncTypeRequired";
- (instancetype)init {
return [self initWithSyncStateFile:kSyncStateFilePath
syncStateAccessAuthorizer:^BOOL() {
// Only access the sync state if a sync server is configured and running as root
return self.syncBaseURL != nil && geteuid() == 0;
}];
}
- (instancetype)initWithSyncStateFile:(NSString *)syncStateFilePath
syncStateAccessAuthorizer:(BOOL (^)(void))syncStateAccessAuthorizer {
self = [super init];
if (self) {
Class number = [NSNumber class];
@@ -164,7 +197,8 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kRemountUSBModeKey : array,
kFullSyncLastSuccess : date,
kRuleSyncLastSuccess : date,
kSyncCleanRequired : number,
kSyncCleanRequiredDeprecated : number,
kSyncTypeRequired : number,
kEnableAllEventUploadKey : number,
kOverrideFileAccessActionKey : string,
};
@@ -181,6 +215,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kBlockedPathRegexKeyDeprecated : re,
kBlockUSBMountKey : number,
kRemountUSBModeKey : array,
kOnStartUSBOptions : string,
kEnablePageZeroProtectionKey : number,
kEnableBadSignatureProtectionKey : number,
kEnableSilentModeKey : number,
@@ -238,12 +273,24 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kEnableAllEventUploadKey : number,
kDisableUnknownEventUploadKey : number,
kOverrideFileAccessActionKey : string,
kEntitlementsPrefixFilterKey : array,
kEntitlementsTeamIDFilterKey : array,
};
_syncStateFilePath = syncStateFilePath;
_syncStateAccessAuthorizerBlock = syncStateAccessAuthorizer;
_defaults = [NSUserDefaults standardUserDefaults];
[_defaults addSuiteNamed:@"com.google.santa"];
_configState = [self readForcedConfig];
[self cacheStaticRules];
_syncState = [self readSyncStateFromDisk] ?: [NSMutableDictionary dictionary];
if ([self migrateDeprecatedSyncStateKeys]) {
// Save the updated sync state if any keys were migrated.
[self saveSyncStateToDisk];
}
_debugFlag = [[NSProcessInfo processInfo].arguments containsObject:@"--debug"];
[self startWatchingDefaults];
}
@@ -409,7 +456,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self syncStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingSyncCleanRequired {
+ (NSSet *)keyPathsForValuesAffectingSyncTypeRequired {
return [self syncStateSet];
}
@@ -525,6 +572,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 +690,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;
}
@@ -776,12 +847,12 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
[self updateSyncStateForKey:kRuleSyncLastSuccess value:ruleSyncLastSuccess];
}
- (BOOL)syncCleanRequired {
return [self.syncState[kSyncCleanRequired] boolValue];
- (SNTSyncType)syncTypeRequired {
return (SNTSyncType)[self.syncState[kSyncTypeRequired] integerValue];
}
- (void)setSyncCleanRequired:(BOOL)syncCleanRequired {
[self updateSyncStateForKey:kSyncCleanRequired value:@(syncCleanRequired)];
- (void)setSyncTypeRequired:(SNTSyncType)syncTypeRequired {
[self updateSyncStateForKey:kSyncTypeRequired value:@(syncTypeRequired)];
}
- (NSString *)machineOwner {
@@ -1053,12 +1124,12 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
/// Read the saved syncState.
///
- (NSMutableDictionary *)readSyncStateFromDisk {
// Only read the sync state if a sync server is configured.
if (!self.syncBaseURL) return nil;
// Only santad should read this file.
if (geteuid() != 0) return nil;
if (!self.syncStateAccessAuthorizerBlock()) {
return nil;
}
NSMutableDictionary *syncState =
[NSMutableDictionary dictionaryWithContentsOfFile:kSyncStateFilePath];
[NSMutableDictionary dictionaryWithContentsOfFile:self.syncStateFilePath];
for (NSString *key in syncState.allKeys) {
if (self.syncServerKeyTypes[key] == [NSRegularExpression class]) {
NSString *pattern = [syncState[key] isKindOfClass:[NSString class]] ? syncState[key] : nil;
@@ -1068,24 +1139,54 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
continue;
}
}
return syncState;
}
///
/// Migrate any deprecated sync state keys/values to alternative keys/values.
///
/// Returns YES if any keys were migrated. Otherwise NO.
///
- (BOOL)migrateDeprecatedSyncStateKeys {
// Currently only one key to migrate
if (!self.syncState[kSyncCleanRequiredDeprecated]) {
return NO;
}
NSMutableDictionary *syncState = self.syncState.mutableCopy;
// If the kSyncTypeRequired key exists, its current value will take precedence.
// Otherwise, migrate the old value to be compatible with the new logic.
if (!self.syncState[kSyncTypeRequired]) {
syncState[kSyncTypeRequired] = [self.syncState[kSyncCleanRequiredDeprecated] boolValue]
? @(SNTSyncTypeClean)
: @(SNTSyncTypeNormal);
}
// Delete the deprecated key
syncState[kSyncCleanRequiredDeprecated] = nil;
self.syncState = syncState;
return YES;
}
///
/// Saves the current effective syncState to disk.
///
- (void)saveSyncStateToDisk {
// Only save the sync state if a sync server is configured.
if (!self.syncBaseURL) return;
// Only santad should write to this file.
if (geteuid() != 0) return;
if (!self.syncStateAccessAuthorizerBlock()) {
return;
}
// Either remove
NSMutableDictionary *syncState = self.syncState.mutableCopy;
syncState[kAllowedPathRegexKey] = [syncState[kAllowedPathRegexKey] pattern];
syncState[kBlockedPathRegexKey] = [syncState[kBlockedPathRegexKey] pattern];
[syncState writeToFile:kSyncStateFilePath atomically:YES];
[syncState writeToFile:self.syncStateFilePath atomically:YES];
[[NSFileManager defaultManager] setAttributes:@{NSFilePosixPermissions : @0600}
ofItemAtPath:kSyncStateFilePath
ofItemAtPath:self.syncStateFilePath
error:NULL];
}
@@ -1093,6 +1194,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,102 @@
/// Copyright 2024 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>
#import <XCTest/XCTest.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
@interface SNTConfigurator (Testing)
- (instancetype)initWithSyncStateFile:(NSString *)syncStateFilePath
syncStateAccessAuthorizer:(BOOL (^)(void))syncStateAccessAuthorizer;
@property NSDictionary *syncState;
@end
@interface SNTConfiguratorTest : XCTestCase
@property NSFileManager *fileMgr;
@property NSString *testDir;
@end
@implementation SNTConfiguratorTest
- (void)setUp {
self.fileMgr = [NSFileManager defaultManager];
self.testDir =
[NSString stringWithFormat:@"%@santa-configurator-%d", NSTemporaryDirectory(), getpid()];
XCTAssertTrue([self.fileMgr createDirectoryAtPath:self.testDir
withIntermediateDirectories:YES
attributes:nil
error:nil]);
}
- (void)tearDown {
XCTAssertTrue([self.fileMgr removeItemAtPath:self.testDir error:nil]);
}
- (void)runMigrationTestsWithSyncState:(NSDictionary *)syncStatePlist
verifier:(void (^)(SNTConfigurator *))verifierBlock {
NSString *syncStatePlistPath =
[NSString stringWithFormat:@"%@/test-sync-state.plist", self.testDir];
XCTAssertTrue([syncStatePlist writeToFile:syncStatePlistPath atomically:YES]);
SNTConfigurator *cfg = [[SNTConfigurator alloc] initWithSyncStateFile:syncStatePlistPath
syncStateAccessAuthorizer:^{
// Allow all access to the test plist
return YES;
}];
NSLog(@"sync state: %@", cfg.syncState);
verifierBlock(cfg);
XCTAssertTrue([self.fileMgr removeItemAtPath:syncStatePlistPath error:nil]);
}
- (void)testInitMigratesSyncStateKeys {
// SyncCleanRequired = YES
[self runMigrationTestsWithSyncState:@{@"SyncCleanRequired" : [NSNumber numberWithBool:YES]}
verifier:^(SNTConfigurator *cfg) {
XCTAssertEqual(cfg.syncState.count, 1);
XCTAssertNil(cfg.syncState[@"SyncCleanRequired"]);
XCTAssertNotNil(cfg.syncState[@"SyncTypeRequired"]);
XCTAssertEqual([cfg.syncState[@"SyncTypeRequired"] integerValue],
SNTSyncTypeClean);
XCTAssertEqual(cfg.syncState.count, 1);
}];
// SyncCleanRequired = NO
[self runMigrationTestsWithSyncState:@{@"SyncCleanRequired" : [NSNumber numberWithBool:NO]}
verifier:^(SNTConfigurator *cfg) {
XCTAssertEqual(cfg.syncState.count, 1);
XCTAssertNil(cfg.syncState[@"SyncCleanRequired"]);
XCTAssertNotNil(cfg.syncState[@"SyncTypeRequired"]);
XCTAssertEqual([cfg.syncState[@"SyncTypeRequired"] integerValue],
SNTSyncTypeNormal);
XCTAssertEqual(cfg.syncState.count, 1);
}];
// Empty state
[self runMigrationTestsWithSyncState:@{}
verifier:^(SNTConfigurator *cfg) {
XCTAssertEqual(cfg.syncState.count, 0);
XCTAssertNil(cfg.syncState[@"SyncCleanRequired"]);
XCTAssertNil(cfg.syncState[@"SyncTypeRequired"]);
}];
}
@end

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

@@ -15,7 +15,7 @@
#import <Foundation/Foundation.h>
// The callback type when KVO notifications are received for observed key paths.
// The first parameter is the previous value, the second paramter is the new value.
// The first parameter is the previous value, the second parameter is the new value.
typedef void (^KVOCallback)(id oldValue, id newValue);
@interface SNTKVOManager : NSObject

View File

@@ -13,7 +13,8 @@
/// limitations under the License.
#import <XCTest/XCTest.h>
#include "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTSyncConstants.h"
#import "Source/common/SNTRule.h"

View File

@@ -32,7 +32,8 @@ extern NSString *const kClientModeMonitor;
extern NSString *const kClientModeLockdown;
extern NSString *const kBlockUSBMount;
extern NSString *const kRemountUSBMode;
extern NSString *const kCleanSync;
extern NSString *const kCleanSyncDeprecated;
extern NSString *const kSyncType;
extern NSString *const kAllowedPathRegex;
extern NSString *const kAllowedPathRegexDeprecated;
extern NSString *const kBlockedPathRegex;

View File

@@ -32,7 +32,8 @@ NSString *const kBlockUSBMount = @"block_usb_mount";
NSString *const kRemountUSBMode = @"remount_usb_mode";
NSString *const kClientModeMonitor = @"MONITOR";
NSString *const kClientModeLockdown = @"LOCKDOWN";
NSString *const kCleanSync = @"clean_sync";
NSString *const kCleanSyncDeprecated = @"clean_sync";
NSString *const kSyncType = @"sync_type";
NSString *const kAllowedPathRegex = @"allowed_path_regex";
NSString *const kAllowedPathRegexDeprecated = @"whitelist_regex";
NSString *const kBlockedPathRegex = @"blocked_path_regex";

View File

@@ -28,7 +28,7 @@
/// Database ops
///
- (void)databaseRuleAddRules:(NSArray *)rules
cleanSlate:(BOOL)cleanSlate
ruleCleanup:(SNTRuleCleanup)cleanupType
reply:(void (^)(NSError *error))reply;
- (void)databaseEventsPending:(void (^)(NSArray *events))reply;
- (void)databaseRemoveEventsWithIDs:(NSArray *)ids;
@@ -45,7 +45,7 @@
- (void)setClientMode:(SNTClientMode)mode reply:(void (^)(void))reply;
- (void)setFullSyncLastSuccess:(NSDate *)date reply:(void (^)(void))reply;
- (void)setRuleSyncLastSuccess:(NSDate *)date reply:(void (^)(void))reply;
- (void)setSyncCleanRequired:(BOOL)cleanReqd reply:(void (^)(void))reply;
- (void)setSyncTypeRequired:(SNTSyncType)syncType reply:(void (^)(void))reply;
- (void)setAllowedPathRegex:(NSString *)pattern reply:(void (^)(void))reply;
- (void)setBlockedPathRegex:(NSString *)pattern reply:(void (^)(void))reply;
- (void)setBlockUSBMount:(BOOL)enabled reply:(void (^)(void))reply;

View File

@@ -50,7 +50,7 @@ NSString *const kBundleID = @"com.google.santa.daemon";
ofReply:YES];
[r setClasses:[NSSet setWithObjects:[NSArray class], [SNTRule class], nil]
forSelector:@selector(databaseRuleAddRules:cleanSlate:reply:)
forSelector:@selector(databaseRuleAddRules:ruleCleanup:reply:)
argumentIndex:0
ofReply:NO];

View File

@@ -44,7 +44,7 @@
// Pass true to isClean to perform a clean sync, defaults to false.
//
- (void)syncWithLogListener:(NSXPCListenerEndpoint *)logListener
isClean:(BOOL)cleanSync
syncType:(SNTSyncType)syncType
reply:(void (^)(SNTSyncStatusType))reply;
// Spindown the syncservice. The syncservice will not automatically start back up.

View File

@@ -68,9 +68,11 @@
- (void)clientMode:(void (^)(SNTClientMode))reply;
- (void)fullSyncLastSuccess:(void (^)(NSDate *))reply;
- (void)ruleSyncLastSuccess:(void (^)(NSDate *))reply;
- (void)syncCleanRequired:(void (^)(BOOL))reply;
- (void)syncTypeRequired:(void (^)(SNTSyncType))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

@@ -0,0 +1,29 @@
/// 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.
#ifndef SANTA__COMMON__SCOPEDCFTYPEREF_H
#define SANTA__COMMON__SCOPEDCFTYPEREF_H
#include <CoreFoundation/CoreFoundation.h>
#include "Source/common/ScopedTypeRef.h"
namespace santa::common {
template <typename CFT>
using ScopedCFTypeRef = ScopedTypeRef<CFT, (CFT)NULL, CFRetain, CFRelease>;
} // namespace santa::common
#endif

View File

@@ -0,0 +1,141 @@
/// 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.
#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>
#import <XCTest/XCTest.h>
#include "XCTest/XCTest.h"
#include "Source/common/ScopedCFTypeRef.h"
using santa::common::ScopedCFTypeRef;
@interface ScopedCFTypeRefTest : XCTestCase
@end
@implementation ScopedCFTypeRefTest
- (void)testDefaultConstruction {
// Default construction creates wraps a NULL object
ScopedCFTypeRef<CFNumberRef> scopedRef;
XCTAssertFalse(scopedRef.Unsafe());
}
- (void)testOperatorBool {
// Operator bool is `false` when object is null
{
ScopedCFTypeRef<CFNumberRef> scopedNullRef;
XCTAssertFalse(scopedNullRef.Unsafe());
XCTAssertFalse(scopedNullRef);
}
// Operator bool is `true` when object is NOT null
{
int x = 123;
CFNumberRef numRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &x);
ScopedCFTypeRef<CFNumberRef> scopedNumRef = ScopedCFTypeRef<CFNumberRef>::Assume(numRef);
XCTAssertTrue(scopedNumRef.Unsafe());
XCTAssertTrue(scopedNumRef);
}
}
// Note that CFMutableArray is used for testing, even when subtypes aren't
// needed, because it is never optimized into immortal constant values, unlike
// other types.
- (void)testAssume {
int want = 123;
int got = 0;
CFMutableArrayRef array = CFArrayCreateMutable(nullptr, /*capacity=*/0, &kCFTypeArrayCallBacks);
// Baseline state, initial retain count is 1 after object creation
XCTAssertEqual(1, CFGetRetainCount(array));
CFNumberRef numRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &want);
CFArrayAppendValue(array, numRef);
CFRelease(numRef);
XCTAssertEqual(1, CFArrayGetCount(array));
{
ScopedCFTypeRef<CFMutableArrayRef> scopedArray =
ScopedCFTypeRef<CFMutableArrayRef>::Assume(array);
// Ensure ownership was taken, and retain count remains unchanged
XCTAssertTrue(scopedArray.Unsafe());
XCTAssertEqual(1, CFGetRetainCount(scopedArray.Unsafe()));
// Make sure the object contains expected contents
CFMutableArrayRef ref = scopedArray.Unsafe();
XCTAssertEqual(1, CFArrayGetCount(ref));
XCTAssertTrue(
CFNumberGetValue((CFNumberRef)CFArrayGetValueAtIndex(ref, 0), kCFNumberIntType, &got));
XCTAssertEqual(want, got);
}
}
// Note that CFMutableArray is used for testing, even when subtypes aren't
// needed, because it is never optimized into immortal constant values, unlike
// other types.
- (void)testRetain {
int want = 123;
int got = 0;
CFMutableArrayRef array = CFArrayCreateMutable(nullptr, /*capacity=*/0, &kCFTypeArrayCallBacks);
// Baseline state, initial retain count is 1 after object creation
XCTAssertEqual(1, CFGetRetainCount(array));
CFNumberRef numRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &want);
CFArrayAppendValue(array, numRef);
CFRelease(numRef);
XCTAssertEqual(1, CFArrayGetCount(array));
{
ScopedCFTypeRef<CFMutableArrayRef> scopedArray =
ScopedCFTypeRef<CFMutableArrayRef>::Retain(array);
// Ensure ownership was taken, and retain count was incremented
XCTAssertTrue(scopedArray.Unsafe());
XCTAssertEqual(2, CFGetRetainCount(scopedArray.Unsafe()));
// Make sure the object contains expected contents
CFMutableArrayRef ref = scopedArray.Unsafe();
XCTAssertEqual(1, CFArrayGetCount(ref));
XCTAssertTrue(
CFNumberGetValue((CFNumberRef)CFArrayGetValueAtIndex(ref, 0), kCFNumberIntType, &got));
XCTAssertEqual(want, got);
}
// The original `array` object should still be valid due to the extra retain.
// Ensure the retain count has decreased since `scopedArray` went out of scope
XCTAssertEqual(1, CFArrayGetCount(array));
}
- (void)testInto {
ScopedCFTypeRef<CFURLRef> scopedURLRef =
ScopedCFTypeRef<CFURLRef>::Assume(CFURLCreateWithFileSystemPath(
kCFAllocatorDefault, CFSTR("/usr/bin/true"), kCFURLPOSIXPathStyle, YES));
ScopedCFTypeRef<SecStaticCodeRef> scopedCodeRef;
XCTAssertFalse(scopedCodeRef);
SecStaticCodeCreateWithPath(scopedURLRef.Unsafe(), kSecCSDefaultFlags,
scopedCodeRef.InitializeInto());
// Ensure the scoped object was initialized
XCTAssertTrue(scopedCodeRef);
}
@end

View File

@@ -0,0 +1,30 @@
/// 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.
#ifndef SANTA__COMMON__SCOPEDIOOBJECTREF_H
#define SANTA__COMMON__SCOPEDIOOBJECTREF_H
#include <IOKit/IOKitLib.h>
#include "Source/common/ScopedTypeRef.h"
namespace santa::common {
template <typename IOT>
using ScopedIOObjectRef =
ScopedTypeRef<IOT, (IOT)IO_OBJECT_NULL, IOObjectRetain, IOObjectRelease>;
}
#endif

View File

@@ -0,0 +1,104 @@
/// 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.
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/usb/IOUSBLib.h>
#import <XCTest/XCTest.h>
#include "Source/common/ScopedIOObjectRef.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
using santa::common::ScopedIOObjectRef;
using santa::santad::logs::endpoint_security::serializers::Utilities::GetDefaultIOKitCommsPort;
@interface ScopedIOObjectRefTest : XCTestCase
@end
@implementation ScopedIOObjectRefTest
- (void)testDefaultConstruction {
// Default construction creates wraps a NULL object
ScopedIOObjectRef<io_object_t> scopedRef;
XCTAssertFalse(scopedRef.Unsafe());
}
- (void)testOperatorBool {
// Operator bool is `false` when object is null
{
ScopedIOObjectRef<io_object_t> scopedNullRef;
XCTAssertFalse(scopedNullRef.Unsafe());
XCTAssertFalse(scopedNullRef);
}
// Operator bool is `true` when object is NOT null
{
CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOUSBDeviceClassName);
XCTAssertNotEqual((CFMutableDictionaryRef)NULL, matchingDict);
io_service_t service = IOServiceGetMatchingService(GetDefaultIOKitCommsPort(), matchingDict);
ScopedIOObjectRef<io_service_t> scopedServiceRef =
ScopedIOObjectRef<io_service_t>::Assume(service);
XCTAssertTrue(scopedServiceRef.Unsafe());
XCTAssertTrue(scopedServiceRef);
}
}
- (void)testAssume {
CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOUSBDeviceClassName);
XCTAssertNotEqual((CFMutableDictionaryRef)NULL, matchingDict);
io_service_t service = IOServiceGetMatchingService(GetDefaultIOKitCommsPort(), matchingDict);
// Baseline state, initial retain count is 1 after object creation
XCTAssertEqual(1, IOObjectGetUserRetainCount(service));
XCTAssertNotEqual(IO_OBJECT_NULL, service);
{
ScopedIOObjectRef<io_service_t> scopedIORef = ScopedIOObjectRef<io_service_t>::Assume(service);
// Ensure ownership was taken, and retain count remains unchanged
XCTAssertTrue(scopedIORef.Unsafe());
XCTAssertEqual(1, IOObjectGetUserRetainCount(scopedIORef.Unsafe()));
XCTAssertNotEqual(IO_OBJECT_NULL, scopedIORef.Unsafe());
}
}
- (void)testRetain {
CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOUSBDeviceClassName);
XCTAssertNotEqual((CFMutableDictionaryRef)NULL, matchingDict);
io_service_t service = IOServiceGetMatchingService(GetDefaultIOKitCommsPort(), matchingDict);
// Baseline state, initial retain count is 1 after object creation
XCTAssertEqual(1, IOObjectGetUserRetainCount(service));
XCTAssertNotEqual(IO_OBJECT_NULL, service);
{
ScopedIOObjectRef<io_service_t> scopedIORef = ScopedIOObjectRef<io_service_t>::Retain(service);
// Ensure ownership was taken, and retain count was incremented
XCTAssertTrue(scopedIORef.Unsafe());
XCTAssertEqual(2, IOObjectGetUserRetainCount(scopedIORef.Unsafe()));
XCTAssertNotEqual(IO_OBJECT_NULL, scopedIORef.Unsafe());
}
// The original `service` object should still be valid due to the extra retain.
// Ensure the retain count has decreased since `scopedIORef` went out of scope.
XCTAssertEqual(1, IOObjectGetUserRetainCount(service));
}
@end

View File

@@ -0,0 +1,80 @@
/// 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.
#ifndef SANTA__COMMON__SCOPEDTYPEREF_H
#define SANTA__COMMON__SCOPEDTYPEREF_H
#include <CoreFoundation/CoreFoundation.h>
#include <assert.h>
namespace santa::common {
template <typename ElementT, ElementT InvalidV, auto RetainFunc,
auto ReleaseFunc>
class ScopedTypeRef {
public:
ScopedTypeRef() : object_(InvalidV) {}
// Can be implemented safely, but not currently needed
ScopedTypeRef(ScopedTypeRef&& other) = delete;
ScopedTypeRef& operator=(ScopedTypeRef&& rhs) = delete;
ScopedTypeRef(const ScopedTypeRef& other) = delete;
ScopedTypeRef& operator=(const ScopedTypeRef& other) = delete;
// Take ownership of a given object
static ScopedTypeRef<ElementT, InvalidV, RetainFunc, ReleaseFunc> Assume(
ElementT object) {
return ScopedTypeRef<ElementT, InvalidV, RetainFunc, ReleaseFunc>(object);
}
// Retain and take ownership of a given object
static ScopedTypeRef<ElementT, InvalidV, RetainFunc, ReleaseFunc> Retain(
ElementT object) {
if (object) {
RetainFunc(object);
}
return ScopedTypeRef<ElementT, InvalidV, RetainFunc, ReleaseFunc>(object);
}
~ScopedTypeRef() {
if (object_) {
ReleaseFunc(object_);
object_ = InvalidV;
}
}
explicit operator bool() { return object_ != InvalidV; }
ElementT Unsafe() { return object_; }
// This is to be used only to take ownership of objects that are created by
// pass-by-pointer create functions. The object must not already be valid.
// In non-opt builds, this is enforced by an assert that will terminate the
// process.
ElementT* InitializeInto() {
assert(object_ == InvalidV);
return &object_;
}
private:
// Not API.
// Use Assume or Retain static methods.
ScopedTypeRef(ElementT object) : object_(object) {}
ElementT object_;
};
} // namespace santa::common
#endif

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

@@ -244,7 +244,7 @@
<constraints>
<constraint firstAttribute="width" priority="900" constant="112" id="Pec-Pa-4aZ"/>
</constraints>
<buttonCell key="cell" type="push" title="Open Event..." bezelStyle="rounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="X1b-TF-1TL">
<buttonCell key="cell" type="push" title="Open..." bezelStyle="rounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="X1b-TF-1TL">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">

View File

@@ -41,6 +41,7 @@ objc_library(
"Commands/SNTCommandFileInfo.m",
"Commands/SNTCommandMetrics.h",
"Commands/SNTCommandMetrics.m",
"Commands/SNTCommandRule.h",
"Commands/SNTCommandRule.m",
"Commands/SNTCommandStatus.m",
"Commands/SNTCommandSync.m",
@@ -115,6 +116,8 @@ santa_unit_test(
"//Source/common:SNTFileInfo",
"//Source/common:SNTLogging",
"//Source/common:SNTRule",
"//Source/common:SNTStoredEvent",
"//Source/common:SNTXPCBundleServiceInterface",
"//Source/common:SNTXPCControlInterface",
"@MOLCertificate",
"@MOLCodesignChecker",
@@ -145,11 +148,27 @@ santa_unit_test(
],
)
santa_unit_test(
name = "SNTCommandRuleTest",
srcs = [
"Commands/SNTCommandRule.h",
"Commands/SNTCommandRuleTest.mm",
"SNTCommand.h",
"SNTCommandController.h",
],
deps = [
":santactl_lib",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTRule",
],
)
test_suite(
name = "unit_tests",
tests = [
":SNTCommandFileInfoTest",
":SNTCommandMetricsTest",
":SNTCommandRuleTest",
],
visibility = ["//:santa_package_group"],
)

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

@@ -0,0 +1,20 @@
/// Copyright 2024 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>
#import "Source/santactl/SNTCommand.h"
@interface SNTCommandRule : SNTCommand <SNTCommandProtocol>
@end

View File

@@ -23,12 +23,10 @@
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTRule.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/santactl/Commands/SNTCommandRule.h"
#import "Source/santactl/SNTCommand.h"
#import "Source/santactl/SNTCommandController.h"
@interface SNTCommandRule : SNTCommand <SNTCommandProtocol>
@end
@implementation SNTCommandRule
REGISTER_COMMAND_NAME(@"rule")
@@ -54,8 +52,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 +62,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 +171,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 +198,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 +243,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) {
@@ -254,7 +251,7 @@ REGISTER_COMMAND_NAME(@"rule")
[[self.daemonConn remoteObjectProxy]
databaseRuleAddRules:@[ newRule ]
cleanSlate:NO
ruleCleanup:SNTRuleCleanupNone
reply:^(NSError *error) {
if (error) {
printf("Failed to modify rules: %s",
@@ -287,72 +284,100 @@ REGISTER_COMMAND_NAME(@"rule")
}];
}
// IMPORTANT: This method makes no attempt to validate whether or not the data
// in a rule is valid. It merely constructs a string with the given data.
// E.g., TeamID compiler rules are not currently supproted, but if a test rule
// is provided with that state, an appropriate string will be returned.
+ (NSString *)stringifyRule:(SNTRule *)rule withColor:(BOOL)colorize {
NSMutableString *output;
// Rule state is saved as eventState for output colorization down below
SNTEventState eventState = SNTEventStateUnknown;
switch (rule.state) {
case SNTRuleStateUnknown:
output = [@"No rule exists with the given parameters" mutableCopy];
break;
case SNTRuleStateAllow: OS_FALLTHROUGH;
case SNTRuleStateAllowCompiler: OS_FALLTHROUGH;
case SNTRuleStateAllowTransitive:
output = [@"Allowed" mutableCopy];
eventState = SNTEventStateAllow;
break;
case SNTRuleStateBlock: OS_FALLTHROUGH;
case SNTRuleStateSilentBlock:
output = [@"Blocked" mutableCopy];
eventState = SNTEventStateBlock;
break;
case SNTRuleStateRemove: OS_FALLTHROUGH;
default:
output = [NSMutableString stringWithFormat:@"Unexpected rule state: %ld", rule.state];
break;
}
if (rule.state == SNTRuleStateUnknown) {
// No more output to append
return output;
}
[output appendString:@" ("];
switch (rule.type) {
case SNTRuleTypeUnknown: [output appendString:@"Unknown"]; break;
case SNTRuleTypeBinary: [output appendString:@"Binary"]; break;
case SNTRuleTypeSigningID: [output appendString:@"SigningID"]; break;
case SNTRuleTypeCertificate: [output appendString:@"Certificate"]; break;
case SNTRuleTypeTeamID: [output appendString:@"TeamID"]; break;
default:
output = [NSMutableString stringWithFormat:@"Unexpected rule type: %ld", rule.type];
break;
}
// Add additional attributes
switch (rule.state) {
case SNTRuleStateAllowCompiler: [output appendString:@", Compiler"]; break;
case SNTRuleStateAllowTransitive: [output appendString:@", Transitive"]; break;
case SNTRuleStateSilentBlock: [output appendString:@", Silent"]; break;
default: break;
}
[output appendString:@")"];
// Colorize
if (colorize) {
if ((SNTEventStateAllow & eventState)) {
[output insertString:@"\033[32m" atIndex:0];
[output appendString:@"\033[0m"];
} else if ((SNTEventStateBlock & eventState)) {
[output insertString:@"\033[31m" atIndex:0];
[output appendString:@"\033[0m"];
} else {
[output insertString:@"\033[33m" atIndex:0];
[output appendString:@"\033[0m"];
}
}
if (rule.state == SNTRuleStateAllowTransitive) {
NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:rule.timestamp];
[output appendString:[NSString stringWithFormat:@"\nlast access date: %@", [date description]]];
}
return output;
}
- (void)printStateOfRule:(SNTRule *)rule daemonConnection:(MOLXPCConnection *)daemonConn {
id<SNTDaemonControlXPC> rop = [daemonConn synchronousRemoteObjectProxy];
NSString *fileSHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil;
NSString *certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil;
NSString *teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil;
NSString *signingID = (rule.type == SNTRuleTypeSigningID) ? rule.identifier : nil;
__block NSMutableString *output;
[rop decisionForFilePath:nil
fileSHA256:fileSHA256
certificateSHA256:certificateSHA256
teamID:teamID
signingID:signingID
reply:^(SNTEventState s) {
output =
(SNTEventStateAllow & s) ? @"Allowed".mutableCopy : @"Blocked".mutableCopy;
switch (s) {
case SNTEventStateAllowUnknown:
case SNTEventStateBlockUnknown: [output appendString:@" (Unknown)"]; break;
case SNTEventStateAllowBinary:
case SNTEventStateBlockBinary: [output appendString:@" (Binary)"]; break;
case SNTEventStateAllowCertificate:
case SNTEventStateBlockCertificate:
[output appendString:@" (Certificate)"];
break;
case SNTEventStateAllowScope:
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
case SNTEventStateAllowCompiler:
[output appendString:@" (Compiler)"];
break;
case SNTEventStateAllowTransitive:
[output appendString:@" (Transitive)"];
break;
case SNTEventStateAllowTeamID:
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
case SNTEventStateAllowSigningID:
case SNTEventStateBlockSigningID:
[output appendString:@" (SigningID)"];
break;
default: output = @"None".mutableCopy; break;
}
if (isatty(STDOUT_FILENO)) {
if ((SNTEventStateAllow & s)) {
[output insertString:@"\033[32m" atIndex:0];
[output appendString:@"\033[0m"];
} else if ((SNTEventStateBlock & s)) {
[output insertString:@"\033[31m" atIndex:0];
[output appendString:@"\033[0m"];
} else {
[output insertString:@"\033[33m" atIndex:0];
[output appendString:@"\033[0m"];
}
}
}];
__block NSString *output;
[rop databaseRuleForBinarySHA256:fileSHA256
certificateSHA256:certificateSHA256
teamID:teamID
signingID:signingID
reply:^(SNTRule *r) {
if (r.state == SNTRuleStateAllowTransitive) {
NSDate *date =
[NSDate dateWithTimeIntervalSinceReferenceDate:r.timestamp];
[output appendString:[NSString
stringWithFormat:@"\nlast access date: %@",
[date description]]];
}
output = [SNTCommandRule stringifyRule:r
withColor:(isatty(STDOUT_FILENO) == 1)];
}];
printf("%s\n", output.UTF8String);
@@ -396,7 +421,7 @@ REGISTER_COMMAND_NAME(@"rule")
[[self.daemonConn remoteObjectProxy]
databaseRuleAddRules:parsedRules
cleanSlate:NO
ruleCleanup:SNTRuleCleanupNone
reply:^(NSError *error) {
if (error) {
printf("Failed to modify rules: %s",
@@ -426,7 +451,7 @@ REGISTER_COMMAND_NAME(@"rule")
NSMutableArray *rulesAsDicts = [[NSMutableArray alloc] init];
for (SNTRule *rule in rules) {
// Omit transitive and remove rules as they're not relevan.
// Omit transitive and remove rules as they're not relevant.
if (rule.state == SNTRuleStateAllowTransitive || rule.state == SNTRuleStateRemove) {
continue;
}

View File

@@ -0,0 +1,96 @@
/// Copyright 2024 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>
#import <XCTest/XCTest.h>
#include <map>
#include <utility>
#import "Source/common/SNTRule.h"
#import "Source/santactl/Commands/SNTCommandRule.h"
@interface SNTCommandRule (Testing)
+ (NSString *)stringifyRule:(SNTRule *)rule withColor:(BOOL)colorize;
@end
@interface SNTRule ()
@property(readwrite) NSUInteger timestamp;
@end
@interface SNTCommandRuleTest : XCTestCase
@end
@implementation SNTCommandRuleTest
- (void)testStringifyRule {
std::map<std::pair<SNTRuleType, SNTRuleState>, NSString *> ruleCheckToString = {
{{SNTRuleTypeUnknown, SNTRuleStateUnknown}, @"No rule exists with the given parameters"},
{{SNTRuleTypeUnknown, SNTRuleStateAllow}, @"Allowed (Unknown)"},
{{SNTRuleTypeUnknown, SNTRuleStateBlock}, @"Blocked (Unknown)"},
{{SNTRuleTypeUnknown, SNTRuleStateSilentBlock}, @"Blocked (Unknown, Silent)"},
{{SNTRuleTypeUnknown, SNTRuleStateRemove}, @"Unexpected rule state: 4 (Unknown)"},
{{SNTRuleTypeUnknown, SNTRuleStateAllowCompiler}, @"Allowed (Unknown, Compiler)"},
{{SNTRuleTypeUnknown, SNTRuleStateAllowTransitive},
@"Allowed (Unknown, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"},
{{SNTRuleTypeBinary, SNTRuleStateUnknown}, @"No rule exists with the given parameters"},
{{SNTRuleTypeBinary, SNTRuleStateAllow}, @"Allowed (Binary)"},
{{SNTRuleTypeBinary, SNTRuleStateBlock}, @"Blocked (Binary)"},
{{SNTRuleTypeBinary, SNTRuleStateSilentBlock}, @"Blocked (Binary, Silent)"},
{{SNTRuleTypeBinary, SNTRuleStateRemove}, @"Unexpected rule state: 4 (Binary)"},
{{SNTRuleTypeBinary, SNTRuleStateAllowCompiler}, @"Allowed (Binary, Compiler)"},
{{SNTRuleTypeBinary, SNTRuleStateAllowTransitive},
@"Allowed (Binary, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"},
{{SNTRuleTypeSigningID, SNTRuleStateUnknown}, @"No rule exists with the given parameters"},
{{SNTRuleTypeSigningID, SNTRuleStateAllow}, @"Allowed (SigningID)"},
{{SNTRuleTypeSigningID, SNTRuleStateBlock}, @"Blocked (SigningID)"},
{{SNTRuleTypeSigningID, SNTRuleStateSilentBlock}, @"Blocked (SigningID, Silent)"},
{{SNTRuleTypeSigningID, SNTRuleStateRemove}, @"Unexpected rule state: 4 (SigningID)"},
{{SNTRuleTypeSigningID, SNTRuleStateAllowCompiler}, @"Allowed (SigningID, Compiler)"},
{{SNTRuleTypeSigningID, SNTRuleStateAllowTransitive},
@"Allowed (SigningID, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"},
{{SNTRuleTypeCertificate, SNTRuleStateUnknown}, @"No rule exists with the given parameters"},
{{SNTRuleTypeCertificate, SNTRuleStateAllow}, @"Allowed (Certificate)"},
{{SNTRuleTypeCertificate, SNTRuleStateBlock}, @"Blocked (Certificate)"},
{{SNTRuleTypeCertificate, SNTRuleStateSilentBlock}, @"Blocked (Certificate, Silent)"},
{{SNTRuleTypeCertificate, SNTRuleStateRemove}, @"Unexpected rule state: 4 (Certificate)"},
{{SNTRuleTypeCertificate, SNTRuleStateAllowCompiler}, @"Allowed (Certificate, Compiler)"},
{{SNTRuleTypeCertificate, SNTRuleStateAllowTransitive},
@"Allowed (Certificate, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"},
{{SNTRuleTypeTeamID, SNTRuleStateUnknown}, @"No rule exists with the given parameters"},
{{SNTRuleTypeTeamID, SNTRuleStateAllow}, @"Allowed (TeamID)"},
{{SNTRuleTypeTeamID, SNTRuleStateBlock}, @"Blocked (TeamID)"},
{{SNTRuleTypeTeamID, SNTRuleStateSilentBlock}, @"Blocked (TeamID, Silent)"},
{{SNTRuleTypeTeamID, SNTRuleStateRemove}, @"Unexpected rule state: 4 (TeamID)"},
{{SNTRuleTypeTeamID, SNTRuleStateAllowCompiler}, @"Allowed (TeamID, Compiler)"},
{{SNTRuleTypeTeamID, SNTRuleStateAllowTransitive},
@"Allowed (TeamID, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"},
};
SNTRule *rule = [[SNTRule alloc] init];
rule.timestamp = 700000000; // time interval since reference date
for (const auto &[typeAndState, want] : ruleCheckToString) {
rule.type = typeAndState.first;
rule.state = typeAndState.second;
NSString *got = [SNTCommandRule stringifyRule:rule withColor:NO];
XCTAssertEqualObjects(got, want);
}
}
@end

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
@@ -119,8 +129,8 @@ REGISTER_COMMAND_NAME(@"status")
}];
__block BOOL syncCleanReqd = NO;
[rop syncCleanRequired:^(BOOL clean) {
syncCleanReqd = clean;
[rop syncTypeRequired:^(SNTSyncType syncType) {
syncCleanReqd = (syncType == SNTSyncTypeClean || syncType == SNTSyncTypeCleanAll);
}];
__block BOOL pushNotifications = NO;
@@ -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];
@@ -185,16 +200,16 @@ REGISTER_COMMAND_NAME(@"status")
@"daemon" : @{
@"driver_connected" : @(YES),
@"mode" : clientMode ?: @"null",
@"transitive_rules" : @(enableTransitiveRules),
@"log_type" : eventLogType,
@"file_logging" : @(fileLogging),
@"watchdog_cpu_events" : @(cpuEvents),
@"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),
@@ -215,7 +230,6 @@ REGISTER_COMMAND_NAME(@"status")
@"last_successful_rule" : ruleSyncLastSuccessStr ?: @"null",
@"push_notifications" : pushNotifications ? @"Connected" : @"Disconnected",
@"bundle_scanning" : @(enableBundles),
@"transitive_rules" : @(enableTransitiveRules),
},
} mutableCopy];
@@ -248,13 +262,20 @@ REGISTER_COMMAND_NAME(@"status")
} else {
printf(">>> Daemon Info\n");
printf(" %-25s | %s\n", "Mode", [clientMode UTF8String]);
if (enableTransitiveRules) {
printf(" %-25s | %s\n", "Transitive Rules", (enableTransitiveRules ? "Yes" : "No"));
}
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);
@@ -294,7 +315,6 @@ REGISTER_COMMAND_NAME(@"status")
printf(" %-25s | %s\n", "Push Notifications",
(pushNotifications ? "Connected" : "Disconnected"));
printf(" %-25s | %s\n", "Bundle Scanning", (enableBundles ? "Yes" : "No"));
printf(" %-25s | %s\n", "Transitive Rules", (enableTransitiveRules ? "Yes" : "No"));
}
if (exportMetrics) {

View File

@@ -32,7 +32,7 @@ REGISTER_COMMAND_NAME(@"sync")
#pragma mark SNTCommand protocol methods
+ (BOOL)requiresRoot {
return YES;
return NO;
}
+ (BOOL)requiresDaemonConn {
@@ -47,8 +47,10 @@ REGISTER_COMMAND_NAME(@"sync")
return (@"If Santa is configured to synchronize with a server, "
@"this is the command used for syncing.\n\n"
@"Options:\n"
@" --clean: Perform a clean sync, erasing all existing rules and requesting a\n"
@" clean sync from the server.");
@" --clean: Perform a clean sync, erasing all existing non-transitive rules and\n"
@" requesting a clean sync from the server.\n"
@" --clean-all: Perform a clean sync, erasing all existing rules and requesting a\n"
@" clean sync from the server.");
}
- (void)runWithArguments:(NSArray *)arguments {
@@ -75,10 +77,17 @@ REGISTER_COMMAND_NAME(@"sync")
lr.unprivilegedInterface =
[NSXPCInterface interfaceWithProtocol:@protocol(SNTSyncServiceLogReceiverXPC)];
[lr resume];
BOOL isClean = [NSProcessInfo.processInfo.arguments containsObject:@"--clean"];
SNTSyncType syncType = SNTSyncTypeNormal;
if ([NSProcessInfo.processInfo.arguments containsObject:@"--clean-all"]) {
syncType = SNTSyncTypeCleanAll;
} else if ([NSProcessInfo.processInfo.arguments containsObject:@"--clean"]) {
syncType = SNTSyncTypeClean;
}
[[ss remoteObjectProxy]
syncWithLogListener:logListener.endpoint
isClean:isClean
syncType:syncType
reply:^(SNTSyncStatusType status) {
if (status == SNTSyncStatusTypeTooManySyncsInProgress) {
[self didReceiveLog:@"Too many syncs in progress, try again later."];

View File

@@ -21,6 +21,9 @@ objc_library(
name = "SNTRuleTable",
srcs = ["DataLayer/SNTRuleTable.m"],
hdrs = ["DataLayer/SNTRuleTable.h"],
sdk_dylibs = [
"EndpointSecurity",
],
deps = [
":SNTDatabaseTable",
"//Source/common:Platform",
@@ -199,6 +202,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 +240,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 +254,9 @@ objc_library(
"//Source/common:SNTStoredEvent",
"//Source/common:SantaVnode",
"//Source/common:String",
"//Source/common:Unit",
"@MOLCodesignChecker",
"@com_google_absl//absl/synchronization",
],
)
@@ -395,8 +403,10 @@ objc_library(
":Metrics",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTDeviceEvent",
"//Source/common:SNTLogging",
"//Source/common:SNTMetricSet",
],
)
@@ -468,7 +478,6 @@ objc_library(
"bsm",
],
deps = [
":EndpointSecurityEnrichedTypes",
":EndpointSecurityMessage",
":SNTDecisionCache",
"//Source/common:SantaCache",
@@ -1317,6 +1326,7 @@ santa_unit_test(
":Metrics",
":MockEndpointSecurityAPI",
":SNTEndpointSecurityDeviceManager",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTDeviceEvent",
"//Source/common:TestUtils",

View File

@@ -75,11 +75,11 @@
/// transaction will abort if any rule fails to add.
///
/// @param rules Array of SNTRule's to add.
/// @param cleanSlate If true, remove all rules before adding the new rules.
/// @param ruleCleanup Rule cleanup type to perform (e.g. all, none, non-transitive).
/// @param error When returning NO, will be filled with appropriate error.
/// @return YES if adding all rules passed, NO if any were rejected.
///
- (BOOL)addRules:(NSArray *)rules cleanSlate:(BOOL)cleanSlate error:(NSError **)error;
- (BOOL)addRules:(NSArray *)rules ruleCleanup:(SNTRuleCleanup)cleanupType error:(NSError **)error;
///
/// Checks the given array of rules to see if adding any of them to the rules database would

View File

@@ -194,7 +194,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
@")"];
[db executeUpdate:@"CREATE UNIQUE INDEX rulesunique ON rules (shasum, type)"];
[[SNTConfigurator configurator] setSyncCleanRequired:YES];
[[SNTConfigurator configurator] setSyncTypeRequired:SNTSyncTypeCleanAll];
newVersion = 1;
}
@@ -403,7 +403,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
#pragma mark Adding
- (BOOL)addRules:(NSArray *)rules
cleanSlate:(BOOL)cleanSlate
ruleCleanup:(SNTRuleCleanup)cleanupType
error:(NSError *__autoreleasing *)error {
if (!rules || rules.count < 1) {
[self fillError:error code:SNTRuleTableErrorEmptyRuleArray message:nil];
@@ -413,8 +413,10 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
__block BOOL failed = NO;
[self inTransaction:^(FMDatabase *db, BOOL *rollback) {
if (cleanSlate) {
if (cleanupType == SNTRuleCleanupAll) {
[db executeUpdate:@"DELETE FROM rules"];
} else if (cleanupType == SNTRuleCleanupNonTransitive) {
[db executeUpdate:@"DELETE FROM rules WHERE state != ?", @(SNTRuleStateAllowTransitive)];
}
for (SNTRule *rule in rules) {

View File

@@ -65,6 +65,15 @@
return r;
}
- (SNTRule *)_exampleTransitiveRule {
SNTRule *r = [[SNTRule alloc] init];
r.identifier = @"1111e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b111";
r.state = SNTRuleStateAllowTransitive;
r.type = SNTRuleTypeBinary;
r.customMsg = @"Transitive rule";
return r;
}
- (SNTRule *)_exampleCertRule {
SNTRule *r = [[SNTRule alloc] init];
r.identifier = @"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258";
@@ -78,7 +87,7 @@
NSUInteger binaryRuleCount = self.sut.binaryRuleCount;
NSError *error;
[self.sut addRules:@[ [self _exampleBinaryRule] ] cleanSlate:NO error:&error];
[self.sut addRules:@[ [self _exampleBinaryRule] ] ruleCleanup:SNTRuleCleanupNone error:&error];
XCTAssertEqual(self.sut.ruleCount, ruleCount + 1);
XCTAssertEqual(self.sut.binaryRuleCount, binaryRuleCount + 1);
@@ -88,24 +97,49 @@
- (void)testAddRulesClean {
// Add a binary rule without clean slate
NSError *error = nil;
XCTAssertTrue([self.sut addRules:@[ [self _exampleBinaryRule] ] cleanSlate:NO error:&error]);
XCTAssertTrue([self.sut addRules:@[ [self _exampleBinaryRule] ]
ruleCleanup:SNTRuleCleanupNone
error:&error]);
XCTAssertNil(error);
// Now add a cert rule with a clean slate, assert that the binary rule was removed
error = nil;
XCTAssertTrue(([self.sut addRules:@[ [self _exampleCertRule] ] cleanSlate:YES error:&error]));
XCTAssertTrue(([self.sut addRules:@[ [self _exampleCertRule] ]
ruleCleanup:SNTRuleCleanupAll
error:&error]));
XCTAssertEqual([self.sut binaryRuleCount], 0);
XCTAssertNil(error);
}
- (void)testAddRulesCleanNonTransitive {
// Add a multiple binary rules, including a transitive rule
NSError *error = nil;
XCTAssertTrue(([self.sut addRules:@[
[self _exampleBinaryRule], [self _exampleCertRule], [self _exampleTransitiveRule]
]
ruleCleanup:SNTRuleCleanupNone
error:&error]));
XCTAssertEqual([self.sut binaryRuleCount], 2);
XCTAssertNil(error);
// Now add a cert rule while cleaning non-transitive rules. Ensure the transitive rule remains
error = nil;
XCTAssertTrue(([self.sut addRules:@[ [self _exampleCertRule] ]
ruleCleanup:SNTRuleCleanupNonTransitive
error:&error]));
XCTAssertEqual([self.sut binaryRuleCount], 1);
XCTAssertEqual([self.sut certificateRuleCount], 1);
XCTAssertNil(error);
}
- (void)testAddMultipleRules {
NSUInteger ruleCount = self.sut.ruleCount;
NSError *error;
[self.sut
addRules:@[ [self _exampleBinaryRule], [self _exampleCertRule], [self _exampleBinaryRule] ]
cleanSlate:NO
error:&error];
addRules:@[ [self _exampleBinaryRule], [self _exampleCertRule], [self _exampleBinaryRule] ]
ruleCleanup:SNTRuleCleanupNone
error:&error];
XCTAssertEqual(self.sut.ruleCount, ruleCount + 2);
XCTAssertNil(error);
@@ -113,13 +147,13 @@
- (void)testAddRulesEmptyArray {
NSError *error;
XCTAssertFalse([self.sut addRules:@[] cleanSlate:YES error:&error]);
XCTAssertFalse([self.sut addRules:@[] ruleCleanup:SNTRuleCleanupAll error:&error]);
XCTAssertEqual(error.code, SNTRuleTableErrorEmptyRuleArray);
}
- (void)testAddRulesNilArray {
NSError *error;
XCTAssertFalse([self.sut addRules:nil cleanSlate:YES error:&error]);
XCTAssertFalse([self.sut addRules:nil ruleCleanup:SNTRuleCleanupAll error:&error]);
XCTAssertEqual(error.code, SNTRuleTableErrorEmptyRuleArray);
}
@@ -129,13 +163,13 @@
r.type = SNTRuleTypeCertificate;
NSError *error;
XCTAssertFalse([self.sut addRules:@[ r ] cleanSlate:NO error:&error]);
XCTAssertFalse([self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error]);
XCTAssertEqual(error.code, SNTRuleTableErrorInvalidRule);
}
- (void)testFetchBinaryRule {
[self.sut addRules:@[ [self _exampleBinaryRule], [self _exampleCertRule] ]
cleanSlate:NO
ruleCleanup:SNTRuleCleanupNone
error:nil];
SNTRule *r = [self.sut
@@ -158,7 +192,7 @@
- (void)testFetchCertificateRule {
[self.sut addRules:@[ [self _exampleBinaryRule], [self _exampleCertRule] ]
cleanSlate:NO
ruleCleanup:SNTRuleCleanupNone
error:nil];
SNTRule *r = [self.sut
@@ -181,7 +215,7 @@
- (void)testFetchTeamIDRule {
[self.sut addRules:@[ [self _exampleBinaryRule], [self _exampleTeamIDRule] ]
cleanSlate:NO
ruleCleanup:SNTRuleCleanupNone
error:nil];
SNTRule *r = [self.sut ruleForBinarySHA256:nil
@@ -205,7 +239,7 @@
[self _exampleBinaryRule], [self _exampleSigningIDRuleIsPlatform:YES],
[self _exampleSigningIDRuleIsPlatform:NO]
]
cleanSlate:NO
ruleCleanup:SNTRuleCleanupNone
error:nil];
XCTAssertEqual([self.sut signingIDRuleCount], 2);
@@ -236,7 +270,7 @@
[self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule],
[self _exampleSigningIDRuleIsPlatform:NO]
]
cleanSlate:NO
ruleCleanup:SNTRuleCleanupNone
error:nil];
// This test verifies that the implicit rule ordering we've been abusing is still working.
@@ -295,7 +329,7 @@
FMDatabaseQueue *dbq = [[FMDatabaseQueue alloc] initWithPath:dbPath];
SNTRuleTable *sut = [[SNTRuleTable alloc] initWithDatabaseQueue:dbq];
[sut addRules:@[ [self _exampleBinaryRule] ] cleanSlate:NO error:nil];
[sut addRules:@[ [self _exampleBinaryRule] ] ruleCleanup:SNTRuleCleanupNone error:nil];
XCTAssertGreaterThan(sut.ruleCount, 0);
[[NSFileManager defaultManager] removeItemAtPath:dbPath error:NULL];
@@ -311,7 +345,7 @@
[self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule],
[self _exampleSigningIDRuleIsPlatform:NO]
]
cleanSlate:NO
ruleCleanup:SNTRuleCleanupNone
error:nil];
NSArray<SNTRule *> *rules = [self.sut retrieveAllRules];

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

@@ -22,7 +22,7 @@
#include <memory>
#include <set>
#include "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTCommonEnums.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/EventProviders/AuthResultCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"

View File

@@ -125,6 +125,11 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
self->_esClient = self->_esApi->NewClient(^(es_client_t *c, Message esMsg) {
int64_t processingStart = clock_gettime_nsec_np(CLOCK_MONOTONIC);
// Update event stats BEFORE calling into the processor class to ensure
// sequence numbers are processed in order.
self->_metrics->UpdateEventStats(self->_processor, esMsg.operator->());
es_event_type_t eventType = esMsg->event_type;
if ([self shouldHandleMessage:esMsg]) {
[self handleMessage:std::move(esMsg)

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

@@ -33,7 +33,7 @@
#include "Source/common/Platform.h"
#include "Source/common/SNTCachedDecision.h"
#include "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/DataLayer/WatchItemPolicy.h"

View File

@@ -134,6 +134,13 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
[self.compilerController handleEvent:esMsg withLogger:self->_logger];
if ((esMsg->event_type == ES_EVENT_TYPE_NOTIFY_FORK ||
esMsg->event_type == ES_EVENT_TYPE_NOTIFY_EXIT) &&
self.configurator.enableForkAndExitLogging == NO) {
recordEventMetrics(EventDisposition::kDropped);
return;
}
// Filter file op events matching the prefix tree.
es_file_t *targetFile = GetTargetFileForPrefixTree(&(*esMsg));
if (targetFile != NULL && self->_prefixTree->HasPrefix(targetFile->path.data)) {

View File

@@ -112,9 +112,9 @@ typedef void (^testHelperBlock)(es_message_t *message,
es_file_t targetFileMatchesRegex = MakeESFile("/foo/matches");
es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
- (void)handleMessageWithMatchCalls:(BOOL)regexMatchCalls
withMissCalls:(BOOL)regexFailsMatchCalls
withBlock:(testHelperBlock)testBlock {
- (void)handleMessageShouldLog:(BOOL)shouldLog
shouldRemoveFromCache:(BOOL)shouldRemoveFromCache
withBlock:(testHelperBlock)testBlock {
es_file_t file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&file);
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc, ActionType::Auth);
@@ -127,15 +127,10 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
auto mockEnricher = std::make_shared<MockEnricher>();
if (regexMatchCalls) {
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(std::move(enrichedMsg)));
}
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMatchesRegex)).Times((int)regexMatchCalls);
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMissesRegex))
.Times((int)regexFailsMatchCalls);
if (shouldRemoveFromCache) {
EXPECT_CALL(*mockAuthCache, RemoveFromCache).Times(1);
}
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
// NOTE: Currently unable to create a partial mock of the
@@ -144,7 +139,8 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
// test will mock the `Log` method that is called in the handler block.
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
auto mockLogger = std::make_shared<MockLogger>(nullptr, nullptr);
if (regexMatchCalls) {
if (shouldLog) {
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(std::move(enrichedMsg)));
EXPECT_CALL(*mockLogger, Log).WillOnce(testing::InvokeWithoutArgs(^() {
dispatch_semaphore_signal(sema);
}));
@@ -250,7 +246,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
}]);
};
[self handleMessageWithMatchCalls:NO withMissCalls:NO withBlock:testBlock];
[self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock];
// CLOSE modified, remove from cache, and matches fileChangesRegex
testBlock = ^(
@@ -274,7 +270,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
XCTAssertSemaTrue(*sema, 5, "Log wasn't called within expected time window");
};
[self handleMessageWithMatchCalls:YES withMissCalls:NO withBlock:testBlock];
[self handleMessageShouldLog:YES shouldRemoveFromCache:YES withBlock:testBlock];
// CLOSE modified, remove from cache, but doesn't match fileChangesRegex
testBlock = ^(
@@ -291,7 +287,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
}]);
};
[self handleMessageWithMatchCalls:NO withMissCalls:YES withBlock:testBlock];
[self handleMessageShouldLog:NO shouldRemoveFromCache:YES withBlock:testBlock];
// LINK, Prefix match, bail early
testBlock =
@@ -316,7 +312,57 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
};
[self handleMessageWithMatchCalls:NO withMissCalls:NO withBlock:testBlock];
[self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock];
// EXIT, EnableForkAndExitLogging is false
testBlock =
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics)
{
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_EXIT;
Message msg(mockESApi, esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
OCMExpect([self.mockConfigurator enableForkAndExitLogging]).andReturn(NO);
[recorderClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kDropped);
dispatch_semaphore_signal(*semaMetrics);
}];
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
};
[self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock];
// FORK, EnableForkAndExitLogging is true
testBlock =
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics)
{
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_FORK;
Message msg(mockESApi, esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
OCMExpect([self.mockConfigurator enableForkAndExitLogging]).andReturn(YES);
[recorderClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kProcessed);
dispatch_semaphore_signal(*semaMetrics);
}];
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
};
[self handleMessageShouldLog:YES shouldRemoveFromCache:NO withBlock:testBlock];
XCTAssertTrue(OCMVerifyAll(self.mockConfigurator));
}
- (void)testGetTargetFileForPrefixTree {

View File

@@ -14,7 +14,7 @@
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
#include "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTCommonEnums.h"
#include "Source/common/SNTLogging.h"
#include "Source/common/SNTStoredEvent.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h"

View File

@@ -23,7 +23,7 @@
#include <string_view>
#include <vector>
#include "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTCommonEnums.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"

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

@@ -28,7 +28,7 @@
#include <cstring>
#import "Source/common/SNTCachedDecision.h"
#include "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTStoredEvent.h"
#include "Source/common/TestUtils.h"
@@ -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

@@ -59,6 +59,8 @@ NSString *MountFromName(NSString *path);
es_file_t *GetAllowListTargetFile(
const santa::santad::event_providers::endpoint_security::Message &msg);
const mach_port_t GetDefaultIOKitCommsPort();
} // namespace santa::santad::logs::endpoint_security::serializers::Utilities
#endif

View File

@@ -80,7 +80,7 @@ NSString *OriginalPathForTranslocation(const es_process_t *es_proc) {
return [origURL path];
}
static inline const mach_port_t GetDefaultIOKitCommsPort() {
const mach_port_t GetDefaultIOKitCommsPort() {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return kIOMasterPortDefault;

View File

@@ -53,6 +53,7 @@ enum class FileAccessMetricStatus {
using EventCountTuple = std::tuple<Processor, es_event_type_t, EventDisposition>;
using EventTimesTuple = std::tuple<Processor, es_event_type_t>;
using EventStatsTuple = std::tuple<Processor, es_event_type_t>;
using FileAccessMetricsPolicyVersion = std::string;
using FileAccessMetricsPolicyName = std::string;
using FileAccessEventCountTuple =
@@ -65,8 +66,9 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *faa_event_counts,
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *));
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *drop_counts,
SNTMetricCounter *faa_event_counts, SNTMetricSet *metric_set,
void (^run_on_first_start)(Metrics *));
~Metrics();
@@ -78,6 +80,9 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
// Force an immediate flush and export of metrics
void Export();
// Used for tracking event sequence numbers to determine if drops occured
void UpdateEventStats(Processor processor, const es_message_t *msg);
void SetEventMetrics(Processor processor, es_event_type_t event_type,
EventDisposition disposition, int64_t nanos);
@@ -90,6 +95,11 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
friend class santa::santad::MetricsPeer;
private:
struct SequenceStats {
uint64_t next_seq_num = 0;
int64_t drops = 0;
};
void FlushMetrics();
void ExportLocked(SNTMetricSet *metric_set);
@@ -101,6 +111,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
SNTMetricCounter *event_counts_;
SNTMetricCounter *rate_limit_counts_;
SNTMetricCounter *faa_event_counts_;
SNTMetricCounter *drop_counts_;
SNTMetricSet *metric_set_;
// Tracks whether or not the timer_source should be running.
// This helps manage dispatch source state to ensure the source is not
@@ -117,6 +128,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
std::map<EventTimesTuple, int64_t> event_times_cache_;
std::map<Processor, int64_t> rate_limit_counts_cache_;
std::map<FileAccessEventCountTuple, int64_t> faa_event_counts_cache_;
std::map<EventStatsTuple, SequenceStats> drop_cache_;
};
} // namespace santa::santad

View File

@@ -49,6 +49,7 @@ static NSString *const kEventTypeNotifyLink = @"NotifyLink";
static NSString *const kEventTypeNotifyRename = @"NotifyRename";
static NSString *const kEventTypeNotifyUnlink = @"NotifyUnlink";
static NSString *const kEventTypeNotifyUnmount = @"NotifyUnmount";
static NSString *const kPseudoEventTypeGlobal = @"Global";
static NSString *const kEventDispositionDropped = @"Dropped";
static NSString *const kEventDispositionProcessed = @"Processed";
@@ -103,6 +104,7 @@ NSString *const EventTypeToString(es_event_type_t eventType) {
case ES_EVENT_TYPE_NOTIFY_RENAME: return kEventTypeNotifyRename;
case ES_EVENT_TYPE_NOTIFY_UNLINK: return kEventTypeNotifyUnlink;
case ES_EVENT_TYPE_NOTIFY_UNMOUNT: return kEventTypeNotifyUnmount;
case ES_EVENT_TYPE_LAST: return kPseudoEventTypeGlobal;
default:
[NSException raise:@"Invalid event type" format:@"Invalid event type: %d", eventType];
return nil;
@@ -167,19 +169,24 @@ 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"
]
helpText:@"Count of times a log is emitted from the File Access Authorizer client"];
std::shared_ptr<Metrics> metrics =
std::make_shared<Metrics>(q, timer_source, interval, event_processing_times, event_counts,
rate_limit_counts, faa_event_counts, metric_set, ^(Metrics *metrics) {
SNTRegisterCoreMetrics();
metrics->EstablishConnection();
});
SNTMetricCounter *drop_counts =
[metric_set counterWithName:@"/santa/event_drop_count"
fieldNames:@[ @"Processor", @"Event" ]
helpText:@"Count of the number of drops for each event"];
std::shared_ptr<Metrics> metrics = std::make_shared<Metrics>(
q, timer_source, interval, event_processing_times, event_counts, rate_limit_counts,
faa_event_counts, drop_counts, metric_set, ^(Metrics *metrics) {
SNTRegisterCoreMetrics();
metrics->EstablishConnection();
});
std::weak_ptr<Metrics> weak_metrics(metrics);
dispatch_source_set_event_handler(metrics->timer_source_, ^{
@@ -197,7 +204,8 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
Metrics::Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *faa_event_counts,
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *))
SNTMetricCounter *drop_counts, SNTMetricSet *metric_set,
void (^run_on_first_start)(Metrics *))
: q_(q),
timer_source_(timer_source),
interval_(interval),
@@ -205,6 +213,7 @@ Metrics::Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t in
event_counts_(event_counts),
rate_limit_counts_(rate_limit_counts),
faa_event_counts_(faa_event_counts),
drop_counts_(drop_counts),
metric_set_(metric_set),
run_on_first_start_(run_on_first_start) {
SetInterval(interval_);
@@ -285,7 +294,22 @@ void Metrics::FlushMetrics() {
]];
}
for (auto &[key, stats] : drop_cache_) {
if (stats.drops > 0) {
NSString *processorName = ProcessorToString(std::get<Processor>(key));
NSString *eventName = EventTypeToString(std::get<es_event_type_t>(key));
[drop_counts_ incrementBy:stats.drops forFieldValues:@[ processorName, eventName ]];
// Reset drops to 0, but leave sequence number intact. Sequence numbers
// must persist so that accurate drops can be counted.
stats.drops = 0;
}
}
// Reset the maps so the next cycle begins with a clean state
// IMPORTANT: Do not reset drop_cache_, the sequence numbers must persist
// for accurate accounting
event_counts_cache_ = {};
event_times_cache_ = {};
rate_limit_counts_cache_ = {};
@@ -339,6 +363,41 @@ void Metrics::SetEventMetrics(Processor processor, es_event_type_t event_type,
});
}
void Metrics::UpdateEventStats(Processor processor, const es_message_t *msg) {
dispatch_sync(events_q_, ^{
EventStatsTuple event_stats_key{processor, msg->event_type};
// NB: Using the invalid event type ES_EVENT_TYPE_LAST to store drop counts
// based on the global sequence number.
EventStatsTuple global_stats_key{processor, ES_EVENT_TYPE_LAST};
SequenceStats old_event_stats = drop_cache_[event_stats_key];
SequenceStats old_global_stats = drop_cache_[global_stats_key];
// Sequence number should always increment by 1.
// Drops detected if there is a difference between the current sequence
// number and the expected sequence number
int64_t new_event_drops = msg->seq_num - old_event_stats.next_seq_num;
int64_t new_global_drops = msg->global_seq_num - old_global_stats.next_seq_num;
// Only log one or the other, prefer the event specific log.
// For higher volume event types, we'll normally see the event-specific log eventually
// For lower volume event types, we may only see the global drop message
if (new_event_drops > 0) {
LOGD(@"Drops detected for client: %@, event: %@, drops: %llu", ProcessorToString(processor),
EventTypeToString(msg->event_type), new_event_drops);
} else if (new_global_drops > 0) {
LOGD(@"Drops detected globally for client: %@, drops: %llu", ProcessorToString(processor),
new_global_drops);
}
drop_cache_[event_stats_key] = SequenceStats{.next_seq_num = msg->seq_num + 1,
.drops = old_event_stats.drops + new_event_drops};
drop_cache_[global_stats_key] = SequenceStats{
.next_seq_num = msg->global_seq_num + 1, .drops = old_global_stats.drops + new_global_drops};
});
}
void Metrics::SetRateLimitingMetrics(Processor processor, int64_t events_rate_limited_count) {
dispatch_sync(events_q_, ^{
rate_limit_counts_cache_[processor] += events_rate_limited_count;

View File

@@ -26,6 +26,7 @@
using santa::santad::EventCountTuple;
using santa::santad::EventDisposition;
using santa::santad::EventStatsTuple;
using santa::santad::EventTimesTuple;
using santa::santad::FileAccessEventCountTuple;
using santa::santad::Processor;
@@ -47,12 +48,15 @@ class MetricsPeer : public Metrics {
using Metrics::FlushMetrics;
// Private member variables
using Metrics::drop_cache_;
using Metrics::event_counts_cache_;
using Metrics::event_times_cache_;
using Metrics::faa_event_counts_cache_;
using Metrics::interval_;
using Metrics::rate_limit_counts_cache_;
using Metrics::running_;
using Metrics::SequenceStats;
};
} // namespace santa::santad
@@ -62,9 +66,15 @@ using santa::santad::EventTypeToString;
using santa::santad::FileAccessMetricStatus;
using santa::santad::FileAccessMetricStatusToString;
using santa::santad::FileAccessPolicyDecisionToString;
using santa::santad::Metrics;
using santa::santad::MetricsPeer;
using santa::santad::ProcessorToString;
std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^block)(Metrics *)) {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
return std::make_shared<MetricsPeer>(q, timer, 100, nil, nil, nil, nil, nil, nil, block);
}
@interface MetricsTest : XCTestCase
@property dispatch_queue_t q;
@property dispatch_semaphore_t sema;
@@ -79,11 +89,9 @@ using santa::santad::ProcessorToString;
}
- (void)testStartStop {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
^(santa::santad::Metrics *m) {
dispatch_semaphore_signal(self.sema);
});
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *) {
dispatch_semaphore_signal(self.sema);
});
XCTAssertFalse(metrics->running_);
@@ -115,10 +123,8 @@ using santa::santad::ProcessorToString;
}
- (void)testSetInterval {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
^(santa::santad::Metrics *m){
});
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
});
XCTAssertEqual(100, metrics->interval_);
@@ -164,6 +170,7 @@ using santa::santad::ProcessorToString;
{ES_EVENT_TYPE_NOTIFY_RENAME, @"NotifyRename"},
{ES_EVENT_TYPE_NOTIFY_UNLINK, @"NotifyUnlink"},
{ES_EVENT_TYPE_NOTIFY_UNMOUNT, @"NotifyUnmount"},
{ES_EVENT_TYPE_LAST, @"Global"},
};
for (const auto &kv : eventTypeToString) {
@@ -224,11 +231,8 @@ using santa::santad::ProcessorToString;
- (void)testSetEventMetrics {
int64_t nanos = 1234;
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
});
// Initial maps are empty
XCTAssertEqual(metrics->event_counts_cache_.size(), 0);
@@ -265,11 +269,8 @@ using santa::santad::ProcessorToString;
}
- (void)testSetRateLimitingMetrics {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
});
// Initial map is empty
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 0);
@@ -291,11 +292,8 @@ using santa::santad::ProcessorToString;
}
- (void)testSetFileAccessEventMetrics {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
});
// Initial map is empty
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 0);
@@ -326,11 +324,64 @@ using santa::santad::ProcessorToString;
XCTAssertEqual(metrics->faa_event_counts_cache_[ruleXyz], 1);
}
- (void)testUpdateEventStats {
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXEC, NULL);
esMsg.seq_num = 0;
esMsg.global_seq_num = 0;
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
});
EventStatsTuple eventStats{Processor::kRecorder, ES_EVENT_TYPE_NOTIFY_EXEC};
EventStatsTuple globalStats{Processor::kRecorder, ES_EVENT_TYPE_LAST};
// Map does not initially contain entries
XCTAssertEqual(0, metrics->drop_cache_.size());
metrics->UpdateEventStats(Processor::kRecorder, &esMsg);
// After the first update, 2 entries exist, one for the event, and one for global
XCTAssertEqual(2, metrics->drop_cache_.size());
XCTAssertEqual(1, metrics->drop_cache_[eventStats].next_seq_num);
XCTAssertEqual(0, metrics->drop_cache_[eventStats].drops);
XCTAssertEqual(1, metrics->drop_cache_[globalStats].next_seq_num);
XCTAssertEqual(0, metrics->drop_cache_[globalStats].drops);
// Increment sequence numbers by 1 and check that no drop was detected
esMsg.seq_num++;
esMsg.global_seq_num++;
metrics->UpdateEventStats(Processor::kRecorder, &esMsg);
XCTAssertEqual(2, metrics->drop_cache_.size());
XCTAssertEqual(2, metrics->drop_cache_[eventStats].next_seq_num);
XCTAssertEqual(0, metrics->drop_cache_[eventStats].drops);
XCTAssertEqual(2, metrics->drop_cache_[globalStats].next_seq_num);
XCTAssertEqual(0, metrics->drop_cache_[globalStats].drops);
// Now incremenet sequence numbers by a large amount to trigger drop detection
esMsg.seq_num += 10;
esMsg.global_seq_num += 10;
metrics->UpdateEventStats(Processor::kRecorder, &esMsg);
XCTAssertEqual(2, metrics->drop_cache_.size());
XCTAssertEqual(12, metrics->drop_cache_[eventStats].next_seq_num);
XCTAssertEqual(9, metrics->drop_cache_[eventStats].drops);
XCTAssertEqual(12, metrics->drop_cache_[globalStats].next_seq_num);
XCTAssertEqual(9, metrics->drop_cache_[globalStats].drops);
}
- (void)testFlushMetrics {
id mockEventProcessingTimes = OCMClassMock([SNTMetricInt64Gauge class]);
id mockEventCounts = OCMClassMock([SNTMetricCounter class]);
int64_t nanos = 1234;
// Initial update will have non-zero sequence numbers, triggering drop detection
es_message_t esMsgWithDrops = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXEC, NULL);
esMsgWithDrops.seq_num = 123;
esMsgWithDrops.global_seq_num = 123;
OCMStub([mockEventCounts incrementBy:0 forFieldValues:[OCMArg any]])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *inv) {
@@ -346,7 +397,7 @@ using santa::santad::ProcessorToString;
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics =
std::make_shared<MetricsPeer>(self.q, timer, 100, mockEventProcessingTimes, mockEventCounts,
mockEventCounts, mockEventCounts, nil,
mockEventCounts, mockEventCounts, mockEventCounts, nil,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
@@ -355,6 +406,7 @@ using santa::santad::ProcessorToString;
EventDisposition::kProcessed, nanos);
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN,
EventDisposition::kProcessed, nanos * 2);
metrics->UpdateEventStats(Processor::kRecorder, &esMsgWithDrops);
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 123);
metrics->SetFileAccessEventMetrics("v1.0", "rule_abc", FileAccessMetricStatus::kOK,
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied);
@@ -364,24 +416,40 @@ using santa::santad::ProcessorToString;
XCTAssertEqual(metrics->event_times_cache_.size(), 2);
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 1);
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 1);
XCTAssertEqual(metrics->drop_cache_.size(), 2);
EventStatsTuple eventStats{Processor::kRecorder, esMsgWithDrops.event_type};
EventStatsTuple globalStats{Processor::kRecorder, ES_EVENT_TYPE_LAST};
XCTAssertEqual(metrics->drop_cache_[eventStats].next_seq_num, 124);
XCTAssertEqual(metrics->drop_cache_[eventStats].drops, 123);
XCTAssertEqual(metrics->drop_cache_[globalStats].next_seq_num, 124);
XCTAssertEqual(metrics->drop_cache_[globalStats].drops, 123);
metrics->FlushMetrics();
// After setting two different event metrics, we expect the sema to be hit
// five times - twice each for the event counts and event times maps, and
// once for the rate limit count map.
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (1)");
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (2)");
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (3)");
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (4)");
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (5)");
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (6)");
// Expected call count is 8:
// 2: event counts
// 2: event times
// 1: rate limit
// 1: FAA
// 2: drops (1 event, 1 global)
int expectedCalls = 8;
for (int i = 0; i < expectedCalls; i++) {
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush");
}
// After a flush, map sizes should be reset to 0
XCTAssertEqual(metrics->event_counts_cache_.size(), 0);
XCTAssertEqual(metrics->event_times_cache_.size(), 0);
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 0);
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 0);
// Note: The drop_cache_ should not be reset back to size 0. Instead, each
// entry has the sequence number left intact, but drop counts reset to 0.
XCTAssertEqual(metrics->drop_cache_.size(), 2);
XCTAssertEqual(metrics->drop_cache_[eventStats].next_seq_num, 124);
XCTAssertEqual(metrics->drop_cache_[eventStats].drops, 0);
XCTAssertEqual(metrics->drop_cache_[globalStats].next_seq_num, 124);
XCTAssertEqual(metrics->drop_cache_[globalStats].drops, 0);
}
@end

View File

@@ -132,8 +132,9 @@ 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;
}
@@ -157,7 +158,7 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
// Add the new rule to the rules database.
NSError *err;
if (![ruleTable addRules:@[ rule ] cleanSlate:NO error:&err]) {
if (![ruleTable addRules:@[ rule ] ruleCleanup:SNTRuleCleanupNone error:&err]) {
LOGE(@"unable to add new transitive rule to database: %@", err.localizedDescription);
} else {
logger->LogAllowlist(esMsg, [fi.SHA256 UTF8String]);

View File

@@ -105,17 +105,18 @@ double watchdogRAMPeak = 0;
}
- (void)databaseRuleAddRules:(NSArray *)rules
cleanSlate:(BOOL)cleanSlate
ruleCleanup:(SNTRuleCleanup)cleanupType
reply:(void (^)(NSError *error))reply {
SNTRuleTable *ruleTable = [SNTDatabaseController ruleTable];
// If any rules are added that are not plain allowlist rules, then flush decision cache.
// In particular, the addition of allowlist compiler rules should cause a cache flush.
// We also flush cache if a allowlist compiler rule is replaced with a allowlist rule.
BOOL flushCache = (cleanSlate || [ruleTable addedRulesShouldFlushDecisionCache:rules]);
BOOL flushCache =
((cleanupType != SNTRuleCleanupNone) || [ruleTable addedRulesShouldFlushDecisionCache:rules]);
NSError *error;
[ruleTable addRules:rules cleanSlate:cleanSlate error:&error];
[ruleTable addRules:rules ruleCleanup:cleanupType error:&error];
// Whenever we add rules, we can also check for and remove outdated transitive rules.
[ruleTable removeOutdatedTransitiveRules];
@@ -233,12 +234,12 @@ double watchdogRAMPeak = 0;
reply();
}
- (void)syncCleanRequired:(void (^)(BOOL))reply {
reply([[SNTConfigurator configurator] syncCleanRequired]);
- (void)syncTypeRequired:(void (^)(SNTSyncType))reply {
reply([[SNTConfigurator configurator] syncTypeRequired]);
}
- (void)setSyncCleanRequired:(BOOL)cleanReqd reply:(void (^)(void))reply {
[[SNTConfigurator configurator] setSyncCleanRequired:cleanReqd];
- (void)setSyncTypeRequired:(SNTSyncType)syncType reply:(void (^)(void))reply {
[[SNTConfigurator configurator] setSyncTypeRequired:syncType];
reply();
}
@@ -258,10 +259,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

@@ -18,7 +18,6 @@
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include <dispatch/dispatch.h>
#include "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTCachedDecision.h"
#import "Source/common/SNTCommonEnums.h"
@@ -96,7 +95,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

@@ -17,6 +17,7 @@
#import <MOLCertificate/MOLCertificate.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTFileInfo.h"
#import "Source/common/SNTLogging.h"
@@ -55,7 +56,8 @@
if (uploadEvents.count >= self.syncState.eventBatchSize) break;
}
if (!self.syncState.cleanSync || [[SNTConfigurator configurator] enableCleanSyncEventUpload]) {
if (self.syncState.syncType == SNTSyncTypeNormal ||
[[SNTConfigurator configurator] enableCleanSyncEventUpload]) {
NSDictionary *r = [self performRequest:[self requestWithDictionary:@{kEvents : uploadEvents}]];
if (!r) return NO;

View File

@@ -14,6 +14,7 @@
#import <Foundation/Foundation.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTXPCSyncServiceInterface.h"
@class MOLXPCConnection;
@@ -60,7 +61,7 @@
///
/// Pass true to isClean to perform a clean sync, defaults to false.
///
- (void)syncAndMakeItClean:(BOOL)clean withReply:(void (^)(SNTSyncStatusType))reply;
- (void)syncType:(SNTSyncType)syncType withReply:(void (^)(SNTSyncStatusType))reply;
///
/// Handle SNTSyncServiceXPC messages forwarded from SNTSyncService.

View File

@@ -87,7 +87,7 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
_fullSyncTimer = [self createSyncTimerWithBlock:^{
[self rescheduleTimerQueue:self.fullSyncTimer
secondsFromNow:_pushNotifications.pushNotificationsFullSyncInterval];
[self syncAndMakeItClean:NO withReply:NULL];
[self syncType:SNTSyncTypeNormal withReply:NULL];
}];
_ruleSyncTimer = [self createSyncTimerWithBlock:^{
dispatch_source_set_timer(self.ruleSyncTimer, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER,
@@ -177,19 +177,19 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
[self rescheduleTimerQueue:self.fullSyncTimer secondsFromNow:seconds];
}
- (void)syncAndMakeItClean:(BOOL)clean withReply:(void (^)(SNTSyncStatusType))reply {
- (void)syncType:(SNTSyncType)syncType withReply:(void (^)(SNTSyncStatusType))reply {
if (dispatch_semaphore_wait(self.syncLimiter, DISPATCH_TIME_NOW)) {
if (reply) reply(SNTSyncStatusTypeTooManySyncsInProgress);
return;
}
dispatch_async(self.syncQueue, ^() {
SLOGI(@"Starting sync...");
if (clean) {
if (syncType != SNTSyncTypeNormal) {
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[[self.daemonConn remoteObjectProxy] setSyncCleanRequired:YES
reply:^() {
dispatch_semaphore_signal(sema);
}];
[[self.daemonConn remoteObjectProxy] setSyncTypeRequired:syncType
reply:^() {
dispatch_semaphore_signal(sema);
}];
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC))) {
SLOGE(@"Timeout waiting for daemon");
if (reply) reply(SNTSyncStatusTypeDaemonTimeout);

View File

@@ -42,11 +42,11 @@
}];
}
// Remove clean sync flag if we did a clean sync
if (self.syncState.cleanSync) {
[rop setSyncCleanRequired:NO
reply:^{
}];
// Remove clean sync flag if we did a clean or clean all sync
if (self.syncState.syncType != SNTSyncTypeNormal) {
[rop setSyncTypeRequired:SNTSyncTypeNormal
reply:^{
}];
}
// Update allowlist/blocklist regexes

View File

@@ -33,6 +33,39 @@ static id EnsureType(id val, Class c) {
}
}
/*
Clean Sync Implementation Notes
The clean sync implementation seems a bit complex at first glance, but boils
down to the following rules:
1. If the server says to do a "clean" sync, a "clean" sync is performed, unless the
client specified a "clean all" sync, in which case "clean all" is performed.
2. If the server responded that it is performing a "clean all" sync, a "clean all" is performed.
3. All other server responses result in a "normal" sync.
The following table expands upon the above logic to list most of the permutations:
| Client Sync State | Clean Sync Request? | Server Response | Sync Type Performed |
| ----------------- | ------------------- | ------------------ | ------------------- |
| normal | No | normal OR <empty> | normal |
| normal | No | clean | clean |
| normal | No | clean_all | clean_all |
| normal | No | clean_sync (dep) | clean |
| normal | Yes | New AND Dep Key | Dep key ignored |
| clean | Yes | normal OR <empty> | normal |
| clean | Yes | clean | clean |
| clean | Yes | clean_all | clean_all |
| clean | Yes | clean_sync (dep) | clean |
| clean | Yes | New AND Dep Key | Dep key ignored |
| clean_all | Yes | normal OR <empty> | normal |
| clean_all | Yes | clean | clean_all |
| clean_all | Yes | clean_all | clean_all |
| clean_all | Yes | clean_sync (dep) | clean_all |
| clean_all | Yes | New AND Dep Key | Dep key ignored |
*/
@implementation SNTSyncPreflight
- (NSURL *)stageURL {
@@ -75,14 +108,15 @@ static id EnsureType(id val, Class c) {
}
}];
__block BOOL syncClean = NO;
[rop syncCleanRequired:^(BOOL clean) {
syncClean = clean;
__block SNTSyncType requestSyncType = SNTSyncTypeNormal;
[rop syncTypeRequired:^(SNTSyncType syncTypeRequired) {
requestSyncType = syncTypeRequired;
}];
// If user requested it or we've never had a successful sync, try from a clean slate.
if (syncClean) {
SLOGD(@"Clean sync requested by user");
if (requestSyncType == SNTSyncTypeClean || requestSyncType == SNTSyncTypeCleanAll) {
SLOGD(@"%@ sync requested by user",
(requestSyncType == SNTSyncTypeCleanAll) ? @"Clean All" : @"Clean");
requestDict[kRequestCleanSync] = @YES;
}
@@ -137,9 +171,51 @@ static id EnsureType(id val, Class c) {
self.syncState.overrideFileAccessAction =
EnsureType(resp[kOverrideFileAccessAction], [NSString class]);
if ([EnsureType(resp[kCleanSync], [NSNumber class]) boolValue]) {
// Default sync type is SNTSyncTypeNormal
//
// Logic overview:
// The requested sync type (clean or normal) is merely informative. The server
// can choose to respond with a normal, clean or clean_all.
//
// If the server responds that it will perform a clean sync, santa will
// treat it as either a clean or clean_all depending on which was requested.
//
// The server can also "override" the requested clean operation. If a normal
// sync was requested, but the server responded that it was doing a clean or
// clean_all sync, that will take precedence. Similarly, if only a clean sync
// was requested, the server can force a "clean_all" operation to take place.
self.syncState.syncType = SNTSyncTypeNormal;
// If kSyncType response key exists, it overrides the kCleanSyncDeprecated value
// First check if the kSyncType reponse key exists. If so, it takes precedence
// over the kCleanSyncDeprecated key.
NSString *responseSyncType = [EnsureType(resp[kSyncType], [NSString class]) lowercaseString];
if (responseSyncType) {
if ([responseSyncType isEqualToString:@"clean"]) {
// If the client wants to Clean All, this takes precedence. The server
// cannot override the client wanting to remove all rules.
if (requestSyncType == SNTSyncTypeCleanAll) {
self.syncState.syncType = SNTSyncTypeCleanAll;
} else {
self.syncState.syncType = SNTSyncTypeClean;
}
} else if ([responseSyncType isEqualToString:@"clean_all"]) {
self.syncState.syncType = SNTSyncTypeCleanAll;
}
} else if ([EnsureType(resp[kCleanSyncDeprecated], [NSNumber class]) boolValue]) {
// If the deprecated key is set, the type of sync clean performed should be
// the type that was requested. This must be set appropriately so that it
// can be propagated during the Rule Download stage so SNTRuleTable knows
// which rules to delete.
if (requestSyncType == SNTSyncTypeCleanAll) {
self.syncState.syncType = SNTSyncTypeCleanAll;
} else {
self.syncState.syncType = SNTSyncTypeClean;
}
}
if (self.syncState.syncType != SNTSyncTypeNormal) {
SLOGD(@"Clean sync requested by server");
self.syncState.cleanSync = YES;
}
return YES;

View File

@@ -24,6 +24,15 @@
#import "Source/santasyncservice/SNTSyncLogging.h"
#import "Source/santasyncservice/SNTSyncState.h"
SNTRuleCleanup SyncTypeToRuleCleanup(SNTSyncType syncType) {
switch (syncType) {
case SNTSyncTypeNormal: return SNTRuleCleanupNone;
case SNTSyncTypeClean: return SNTRuleCleanupNonTransitive;
case SNTSyncTypeCleanAll: return SNTRuleCleanupAll;
default: return SNTRuleCleanupNone;
}
}
@implementation SNTSyncRuleDownload
- (NSURL *)stageURL {
@@ -41,12 +50,13 @@
// Wait until finished or until 5 minutes pass.
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
__block NSError *error;
[[self.daemonConn remoteObjectProxy] databaseRuleAddRules:newRules
cleanSlate:self.syncState.cleanSync
reply:^(NSError *e) {
error = e;
dispatch_semaphore_signal(sema);
}];
[[self.daemonConn remoteObjectProxy]
databaseRuleAddRules:newRules
ruleCleanup:SyncTypeToRuleCleanup(self.syncState.syncType)
reply:^(NSError *e) {
error = e;
dispatch_semaphore_signal(sema);
}];
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 300 * NSEC_PER_SEC))) {
SLOGE(@"Failed to add rule(s) to database: timeout sending rules to daemon");
return NO;

Some files were not shown because too many files have changed in this diff Show More