Compare commits

..

73 Commits

Author SHA1 Message Date
Nick Gregory
57fc2b0253 Add missing EndpointSecurity dylib (#1315) 2024-03-25 13:41:20 -04:00
Nick Gregory
262adfecbd Fix BUILD deps (#1314) 2024-03-25 13:19:13 -04:00
Jason McCandless
1606657bb3 Add CDHash to rule evaluation order doc. (#1313) 2024-03-22 18:13:58 -04:00
Matt W
b379819cfa Overrides disabled when running tests unless explicitly enabled (#1312)
* Emit a log warning when overrides were applied

* Overrides now disabled in tests unless explicitly enabled

* Remove log message. Check for xctest instead of bazel env vars.

* Typo
2024-03-22 16:44:45 -04:00
Pete Markowsky
b9f6005411 Fix: Do not flush authcache when receiving duplicate block rules from the sync service (#1310)
* Change the behavior of addedRulesShouldFlushDecisionCache to flush when 1000 non-allowlist rules are added or a remove rule is encountered or any new non-allowlist rules are added

* Add tests for cache flushing behavior.
2024-03-22 11:24:42 -04:00
Russell Hancox
e31aa5cf39 Tests: Fix SNTRuleTableTest in the presence of local static rules (#1311) 2024-03-19 18:06:39 -04:00
Nick Gregory
77d191ae26 ProcessTree: integrate process tree throughout the event processing lifecycle (3/4) (#1281)
* process annotations: thread the tree through santa

* Update enricher to read annotations from the ProcessTree

* rebase changes

* add configuration for annotations, disabling the tree entirely if none are enabled

* lingering build dep

* use tree factory constructor

* fix configurator

* build fixes

* rebase fixes

* fix tests

* review comments

* lint

* english hard

* record metrics even when event only used for process tree
2024-03-14 11:31:51 -04:00
Pete Markowsky
160195a1d4 Implement NSSecureCoding for SNTRuleIdentifiers (#1307)
* Fix an issue with santactl fileinfo by implementing NSSecureCoding for SNTRuleIdentifiers.
2024-03-11 10:03:49 -04:00
Matt W
f2ce92650b Add required dep for internal builds (#1302) 2024-03-05 15:39:59 -05:00
Matt W
e89cdbcf64 Add support for CDHash rule types (#1301)
* Support CDHash rules

* Ensure hardened runtime for cdhash eval. Update docs.

* minor fixups

* Clarify docs
2024-03-05 15:07:36 -05:00
Pete Markowsky
6a697e00ea Added clean flags for JSON rule import (#1300)
* Add --clean and --clean-all flags to the santactl rule command to allow clearing the rule database when importing rules via JSON.
2024-03-03 11:12:53 -05:00
Matt W
74d8fe30d1 Creating transitive rules for rename events should fallback to destination path (#1299)
* Transitive rules should fallback to destination for RENAME events

* Add tests to exercise fallback for rename events
2024-02-28 17:09:07 -05:00
Matt W
7513c75f88 Refactor rule and count lookups (#1298)
* Refactor rule and count lookups

* Remove commented out code

* Change rule count types to int64_t. SNTRuleIdentifiers properties now RO.
2024-02-26 15:09:51 -05:00
Matt W
9bee43130e Make FileChangesRegex apply to all file change event types (#1294)
* Make FileChangesRegex apply to all file change event types

* Handle older SDKs

* Formatting

* Remove debug log
2024-02-22 10:12:02 -05:00
Nick Gregory
7fa23d4b97 Some more lint fixes (#1295)
* lint fixes

* more lint
2024-02-20 15:39:24 -05:00
Nick Gregory
42eb0a3669 ProcessTree: add macOS specific loader and ES adapter (2/4) (#1237)
* ProcessTree: add macos-specific loader and event adapter

* lingering darwin->macos

* lint

* remove defunct client id

* struct rename

* and one last header update

* use EndpointSecurityAPI in adapter

* expose esapi in message
2024-02-20 13:56:54 -05:00
Russell Hancox
1ea26f0ac9 docs: Document that *PathRegex does not work on symlinks (#1290) 2024-02-13 18:53:17 -05:00
Nick Gregory
c35e9978d3 ProcessTree: fix missing direct deps (#1288)
* hmm

* more lint and add another dep
2024-02-09 10:33:57 -05:00
Matt W
e4c0d56bb6 Remove proc tree tests for now as the code isn't yet included in santa builds (#1287) 2024-02-08 16:01:47 -05:00
Matt W
908b1bcabe Add build dep for internal process (#1286) 2024-02-08 15:43:01 -05:00
Matt W
64e81bedc6 Respect fail closed on deadlines (#1285)
* Responses to events about to exceed deadline should respect FailClosed

* Only respect FailClosed when in Lockdown mode. Update docs.

* FailClosed in Configurator now wraps checking client mode

* PR feedback

* Fix execution controller tests with new FailClosed logic
2024-02-08 15:12:05 -05:00
Matt W
5dfab22fa7 Fix automatically denied events with small deadlines (#1284)
* Fix automatically denied events with small deadlines

* Fix up additional tests that had defined deadline interactions
2024-02-08 10:25:06 -05:00
Nick Gregory
5248e2a7eb Fix import issues and lint (#1282)
* lint

* case insensitive filesystems ahhhh

* tidy

* one last header
2024-02-07 17:46:42 -05:00
Nick Gregory
e8db89c57c ProcessTree: add core process tree logic (1/4) (#1236)
* ProcessTree: add core process tree logic

* make Step implicitly called by Handle* methods

* lint

* naming convention

* widen pidversion to be generic

* move os specific backfill to os specific impl

* simplify ts checking

* retain/release a whole vec of pids

* document processtoken

* lint

* namespace

* add process tree to project-wide unit test target

* case change annotations

* case change annotations

* remove stray comment

* default initialize seen_timestamps

* fix missing initialization of refcnt and tombstoned

* reshuffle pb namespace

* pr review

* move annotation registration to tree construction

* use factory function for tree construction
2024-02-05 14:30:54 -05:00
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
170 changed files with 7048 additions and 1223 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

@@ -11,6 +11,7 @@ proto_library(
name = "santa_proto",
srcs = ["santa.proto"],
deps = [
"//Source/santad/ProcessTree:process_tree_proto",
"@com_google_protobuf//:any_proto",
"@com_google_protobuf//:timestamp_proto",
],
@@ -40,6 +41,12 @@ objc_library(
],
)
objc_library(
name = "SNTDeepCopy",
srcs = ["SNTDeepCopy.m"],
hdrs = ["SNTDeepCopy.h"],
)
cc_library(
name = "SantaCache",
hdrs = ["SantaCache.h"],
@@ -54,6 +61,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"],
@@ -207,6 +264,7 @@ objc_library(
hdrs = ["SNTFileInfo.h"],
deps = [
":SNTLogging",
":SantaVnode",
"@FMDB",
"@MOLCodesignChecker",
],
@@ -256,6 +314,12 @@ santa_unit_test(
],
)
objc_library(
name = "SNTRuleIdentifiers",
srcs = ["SNTRuleIdentifiers.m"],
hdrs = ["SNTRuleIdentifiers.h"],
)
objc_library(
name = "SNTStoredEvent",
srcs = ["SNTStoredEvent.m"],
@@ -321,6 +385,7 @@ objc_library(
":SNTCommonEnums",
":SNTConfigurator",
":SNTRule",
":SNTRuleIdentifiers",
":SNTStoredEvent",
":SNTXPCUnprivilegedControlInterface",
"@MOLCodesignChecker",
@@ -363,6 +428,7 @@ objc_library(
deps = [
":SNTCommonEnums",
":SNTRule",
":SNTRuleIdentifiers",
":SNTStoredEvent",
":SNTXPCBundleServiceInterface",
":SantaVnode",
@@ -421,17 +487,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

@@ -25,7 +25,9 @@
///
@interface SNTCachedDecision : NSObject
- (instancetype)init;
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile;
- (instancetype)initWithVnode:(SantaVnode)vnode NS_DESIGNATED_INITIALIZER;
@property SantaVnode vnodeId;
@property SNTEventState decision;
@@ -38,6 +40,9 @@
@property NSArray<MOLCertificate *> *certChain;
@property NSString *teamID;
@property NSString *signingID;
@property NSString *cdhash;
@property NSDictionary *entitlements;
@property BOOL entitlementsFiltered;
@property NSString *quarantineURL;

View File

@@ -17,10 +17,18 @@
@implementation SNTCachedDecision
- (instancetype)init {
return [self initWithVnode:(SantaVnode){}];
}
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile {
return [self initWithVnode:SantaVnode::VnodeForFile(esFile)];
}
- (instancetype)initWithVnode:(SantaVnode)vnode {
self = [super init];
if (self) {
_vnodeId = SantaVnode::VnodeForFile(esFile);
_vnodeId = vnode;
}
return self;
}

View File

@@ -46,6 +46,7 @@ typedef NS_ENUM(NSInteger, SNTAction) {
typedef NS_ENUM(NSInteger, SNTRuleType) {
SNTRuleTypeUnknown = 0,
SNTRuleTypeCDHash = 500,
SNTRuleTypeBinary = 1000,
SNTRuleTypeSigningID = 2000,
SNTRuleTypeCertificate = 3000,
@@ -84,6 +85,7 @@ typedef NS_ENUM(uint64_t, SNTEventState) {
SNTEventStateBlockTeamID = 1ULL << 20,
SNTEventStateBlockLongPath = 1ULL << 21,
SNTEventStateBlockSigningID = 1ULL << 22,
SNTEventStateBlockCDHash = 1ULL << 23,
// Bits 40-63 store allow decision types
SNTEventStateAllowUnknown = 1ULL << 40,
@@ -95,6 +97,7 @@ typedef NS_ENUM(uint64_t, SNTEventState) {
SNTEventStateAllowPendingTransitive = 1ULL << 46,
SNTEventStateAllowTeamID = 1ULL << 47,
SNTEventStateAllowSigningID = 1ULL << 48,
SNTEventStateAllowCDHash = 1ULL << 49,
// Block and Allow masks
SNTEventStateBlock = 0xFFFFFFULL << 16,
@@ -158,6 +161,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

@@ -40,7 +40,8 @@
///
/// Enable Fail Close mode. Defaults to NO.
/// This controls Santa's behavior when a failure occurs, such as an
/// inability to read a file. By default, to prevent bugs or misconfiguration
/// inability to read a file and as a default response when deadlines
/// are about to expire. By default, to prevent bugs or misconfiguration
/// from rendering a machine inoperable Santa will fail open and allow
/// execution. With this setting enabled, Santa will fail closed if the client
/// is in LOCKDOWN mode, offering a higher level of security but with a higher
@@ -284,7 +285,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 +438,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 +450,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 +643,24 @@
///
@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;
///
/// List of enabled process annotations.
/// This property is not KVO compliant.
///
@property(readonly, nonatomic) NSArray<NSString *> *enabledProcessAnnotations;
///
/// 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,21 @@ 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";
static NSString *const kEnabledProcessAnnotations = @"EnabledProcessAnnotations";
// 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 +160,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 +199,8 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kRemountUSBModeKey : array,
kFullSyncLastSuccess : date,
kRuleSyncLastSuccess : date,
kSyncCleanRequired : number,
kSyncCleanRequiredDeprecated : number,
kSyncTypeRequired : number,
kEnableAllEventUploadKey : number,
kOverrideFileAccessActionKey : string,
};
@@ -181,6 +217,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kBlockedPathRegexKeyDeprecated : re,
kBlockUSBMountKey : number,
kRemountUSBModeKey : array,
kOnStartUSBOptions : string,
kEnablePageZeroProtectionKey : number,
kEnableBadSignatureProtectionKey : number,
kEnableSilentModeKey : number,
@@ -238,12 +275,25 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kEnableAllEventUploadKey : number,
kDisableUnknownEventUploadKey : number,
kOverrideFileAccessActionKey : string,
kEntitlementsPrefixFilterKey : array,
kEntitlementsTeamIDFilterKey : array,
kEnabledProcessAnnotations : 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 +459,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self syncStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingSyncCleanRequired {
+ (NSSet *)keyPathsForValuesAffectingSyncTypeRequired {
return [self syncStateSet];
}
@@ -525,6 +575,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 {
@@ -549,8 +607,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
- (BOOL)failClosed {
NSNumber *n = self.configState[kFailClosedKey];
if (n) return [n boolValue];
return NO;
return [n boolValue] && self.clientMode == SNTClientModeLockdown;
}
- (BOOL)enableTransitiveRules {
@@ -635,6 +692,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 +849,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 {
@@ -1035,6 +1108,16 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return self.configState[kMetricExtraLabels];
}
- (NSArray<NSString *> *)enabledProcessAnnotations {
NSArray<NSString *> *annotations = self.configState[kEnabledProcessAnnotations];
for (id annotation in annotations) {
if (![annotation isKindOfClass:[NSString class]]) {
return nil;
}
}
return annotations;
}
#pragma mark Private
///
@@ -1053,12 +1136,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 +1151,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 +1206,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 {
@@ -1101,6 +1222,39 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:NULL];
}
- (void)applyOverrides:(NSMutableDictionary *)forcedConfig {
// Overrides should only be applied under debug builds.
#ifdef DEBUG
if ([[[NSProcessInfo processInfo] processName] isEqualToString:@"xctest"] &&
![[[NSProcessInfo processInfo] environment] objectForKey:@"ENABLE_CONFIG_OVERRIDES"]) {
// By default, config overrides are not applied when running tests to help
// mitigate potential issues due to unexpected config values. This behavior
// can be overriden if desired by using the env variable: `ENABLE_CONFIG_OVERRIDES`.
//
// E.g.:
// bazel test --test_env=ENABLE_CONFIG_OVERRIDES=1 ...other test args...
return;
}
NSDictionary *overrides = [NSDictionary dictionaryWithContentsOfFile:kConfigOverrideFilePath];
for (NSString *key in overrides) {
id obj = overrides[key];
if (![obj isKindOfClass:self.forcedConfigKeyTypes[key]] ||
([self.forcedConfigKeyTypes[key] isKindOfClass:[NSRegularExpression class]] &&
![obj isKindOfClass:[NSString class]])) {
continue;
}
forcedConfig[key] = obj;
if (self.forcedConfigKeyTypes[key] == [NSRegularExpression class]) {
NSString *pattern = [obj isKindOfClass:[NSString class]] ? obj : nil;
forcedConfig[key] = [self expressionForPattern:pattern];
}
}
#endif
}
- (NSMutableDictionary *)readForcedConfig {
NSMutableDictionary *forcedConfig = [NSMutableDictionary dictionary];
for (NSString *key in self.forcedConfigKeyTypes) {
@@ -1112,22 +1266,9 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
forcedConfig[key] = [self expressionForPattern:pattern];
}
}
#ifdef DEBUG
NSDictionary *overrides = [NSDictionary dictionaryWithContentsOfFile:kConfigOverrideFilePath];
for (NSString *key in overrides) {
id obj = overrides[key];
if (![obj isKindOfClass:self.forcedConfigKeyTypes[key]] ||
([self.forcedConfigKeyTypes[key] isKindOfClass:[NSRegularExpression class]] &&
![obj isKindOfClass:[NSString class]])) {
continue;
}
forcedConfig[key] = obj;
if (self.forcedConfigKeyTypes[key] == [NSRegularExpression class]) {
NSString *pattern = [obj isKindOfClass:[NSString class]] ? obj : nil;
forcedConfig[key] = [self expressionForPattern:pattern];
}
}
#endif
[self applyOverrides:forcedConfig];
return forcedConfig;
}

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,6 +15,8 @@
#import <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
#import "Source/common/SantaVnode.h"
@class MOLCodesignChecker;
///
@@ -220,6 +222,11 @@
///
- (NSUInteger)fileSize;
///
/// @return The devno/ino pair of the file
///
- (SantaVnode)vnode;
///
/// @return The underlying file handle.
///

View File

@@ -49,6 +49,7 @@
@property NSString *path;
@property NSFileHandle *fileHandle;
@property NSUInteger fileSize;
@property SantaVnode vnode;
@property NSString *fileOwnerHomeDir;
@property NSString *sha256Storage;
@@ -110,6 +111,7 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
}
_fileSize = fileStat->st_size;
_vnode = (SantaVnode){.fsid = fileStat->st_dev, .fileid = fileStat->st_ino};
if (_fileSize == 0) return nil;

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

@@ -15,6 +15,7 @@
#import "Source/common/SNTRule.h"
#include <CommonCrypto/CommonCrypto.h>
#include <Kernel/kern/cs_blobs.h>
#include <os/base.h>
#import "Source/common/SNTSyncConstants.h"
@@ -103,6 +104,14 @@ static const NSUInteger kExpectedTeamIDLength = 10;
break;
}
case SNTRuleTypeCDHash: {
identifier = [[identifier lowercaseString] stringByTrimmingCharactersInSet:nonHex];
if (identifier.length != CS_CDHASH_LEN * 2) {
return nil;
}
break;
}
default: {
break;
}
@@ -173,6 +182,8 @@ static const NSUInteger kExpectedTeamIDLength = 10;
type = SNTRuleTypeTeamID;
} else if ([ruleTypeString isEqual:kRuleTypeSigningID]) {
type = SNTRuleTypeSigningID;
} else if ([ruleTypeString isEqual:kRuleTypeCDHash]) {
type = SNTRuleTypeCDHash;
} else {
return nil;
}

View File

@@ -0,0 +1,51 @@
/// 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.
/**
* This file declares two types that are mirrors of each other.
*
* The C struct serves as a way to group and pass valid rule identifiers around
* in order to minimize interface changes needed when new rule types are added
* and also alleviate the need to allocate a short lived object.
*
* The Objective C class is used for an XPC boundary to easily pass rule
* identifiers between Santa components.
*/
#import <Foundation/Foundation.h>
struct RuleIdentifiers {
NSString *cdhash;
NSString *binarySHA256;
NSString *signingID;
NSString *certificateSHA256;
NSString *teamID;
};
@interface SNTRuleIdentifiers : NSObject <NSSecureCoding>
@property(readonly) NSString *cdhash;
@property(readonly) NSString *binarySHA256;
@property(readonly) NSString *signingID;
@property(readonly) NSString *certificateSHA256;
@property(readonly) NSString *teamID;
/// Please use `initWithRuleIdentifiers:`
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithRuleIdentifiers:(struct RuleIdentifiers)identifiers
NS_DESIGNATED_INITIALIZER;
- (struct RuleIdentifiers)toStruct;
@end

View File

@@ -0,0 +1,73 @@
/// 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 "Source/common/SNTRuleIdentifiers.h"
@implementation SNTRuleIdentifiers
- (instancetype)initWithRuleIdentifiers:(struct RuleIdentifiers)identifiers {
self = [super init];
if (self) {
_cdhash = identifiers.cdhash;
_binarySHA256 = identifiers.binarySHA256;
_signingID = identifiers.signingID;
_certificateSHA256 = identifiers.certificateSHA256;
_teamID = identifiers.teamID;
}
return self;
}
- (struct RuleIdentifiers)toStruct {
return (struct RuleIdentifiers){.cdhash = self.cdhash,
.binarySHA256 = self.binarySHA256,
.signingID = self.signingID,
.certificateSHA256 = self.certificateSHA256,
.teamID = self.teamID};
}
#pragma mark NSSecureCoding
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-literal-conversion"
#define ENCODE(obj, key) \
if (obj) [coder encodeObject:obj forKey:key]
#define DECODE(cls, key) [decoder decodeObjectOfClass:[cls class] forKey:key]
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)initWithCoder:(NSCoder *)decoder {
self = [self init];
if (self) {
_cdhash = DECODE(NSString, @"cdhash");
_binarySHA256 = DECODE(NSString, @"binarySHA256");
_signingID = DECODE(NSString, @"signingID");
_certificateSHA256 = DECODE(NSString, @"certificateSHA256");
_teamID = DECODE(NSString, @"teamID");
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
ENCODE(self.cdhash, @"cdhash");
ENCODE(self.binarySHA256, @"binarySHA256");
ENCODE(self.signingID, @"signingID");
ENCODE(self.certificateSHA256, @"certificateSHA256");
ENCODE(self.teamID, @"teamID");
}
#pragma clang diagnostic pop
@end

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

@@ -105,6 +105,11 @@
///
@property NSString *signingID;
///
/// If the executed file was signed, this is the CDHash of the binary.
///
@property NSString *cdhash;
///
/// The user who executed the binary.
///

View File

@@ -51,6 +51,7 @@
ENCODE(self.signingChain, @"signingChain");
ENCODE(self.teamID, @"teamID");
ENCODE(self.signingID, @"signingID");
ENCODE(self.cdhash, @"cdhash");
ENCODE(self.executingUser, @"executingUser");
ENCODE(self.occurrenceDate, @"occurrenceDate");
@@ -97,6 +98,7 @@
_signingChain = DECODEARRAY(MOLCertificate, @"signingChain");
_teamID = DECODE(NSString, @"teamID");
_signingID = DECODE(NSString, @"signingID");
_cdhash = DECODE(NSString, @"cdhash");
_executingUser = DECODE(NSString, @"executingUser");
_occurrenceDate = DECODE(NSDate, @"occurrenceDate");

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;
@@ -43,6 +44,7 @@ extern NSString *const kCompilerRuleCount;
extern NSString *const kTransitiveRuleCount;
extern NSString *const kTeamIDRuleCount;
extern NSString *const kSigningIDRuleCount;
extern NSString *const kCDHashRuleCount;
extern NSString *const kFullSyncInterval;
extern NSString *const kFCMToken;
extern NSString *const kFCMFullSyncInterval;
@@ -69,12 +71,14 @@ extern NSString *const kDecisionAllowCertificate;
extern NSString *const kDecisionAllowScope;
extern NSString *const kDecisionAllowTeamID;
extern NSString *const kDecisionAllowSigningID;
extern NSString *const kDecisionAllowCDHash;
extern NSString *const kDecisionBlockUnknown;
extern NSString *const kDecisionBlockBinary;
extern NSString *const kDecisionBlockCertificate;
extern NSString *const kDecisionBlockScope;
extern NSString *const kDecisionBlockTeamID;
extern NSString *const kDecisionBlockSigningID;
extern NSString *const kDecisionBlockCDHash;
extern NSString *const kDecisionUnknown;
extern NSString *const kDecisionBundleBinary;
extern NSString *const kLoggedInUsers;
@@ -100,6 +104,7 @@ extern NSString *const kCertValidFrom;
extern NSString *const kCertValidUntil;
extern NSString *const kTeamID;
extern NSString *const kSigningID;
extern NSString *const kCDHash;
extern NSString *const kQuarantineDataURL;
extern NSString *const kQuarantineRefererURL;
extern NSString *const kQuarantineTimestamp;
@@ -124,6 +129,7 @@ extern NSString *const kRuleTypeBinary;
extern NSString *const kRuleTypeCertificate;
extern NSString *const kRuleTypeTeamID;
extern NSString *const kRuleTypeSigningID;
extern NSString *const kRuleTypeCDHash;
extern NSString *const kRuleCustomMsg;
extern NSString *const kRuleCustomURL;
extern NSString *const kCursor;

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";
@@ -43,6 +44,7 @@ NSString *const kCompilerRuleCount = @"compiler_rule_count";
NSString *const kTransitiveRuleCount = @"transitive_rule_count";
NSString *const kTeamIDRuleCount = @"teamid_rule_count";
NSString *const kSigningIDRuleCount = @"signingid_rule_count";
NSString *const kCDHashRuleCount = @"cdhash_rule_count";
NSString *const kFullSyncInterval = @"full_sync_interval";
NSString *const kFCMToken = @"fcm_token";
NSString *const kFCMFullSyncInterval = @"fcm_full_sync_interval";
@@ -70,12 +72,14 @@ NSString *const kDecisionAllowCertificate = @"ALLOW_CERTIFICATE";
NSString *const kDecisionAllowScope = @"ALLOW_SCOPE";
NSString *const kDecisionAllowTeamID = @"ALLOW_TEAMID";
NSString *const kDecisionAllowSigningID = @"ALLOW_SIGNINGID";
NSString *const kDecisionAllowCDHash = @"ALLOW_CDHASH";
NSString *const kDecisionBlockUnknown = @"BLOCK_UNKNOWN";
NSString *const kDecisionBlockBinary = @"BLOCK_BINARY";
NSString *const kDecisionBlockCertificate = @"BLOCK_CERTIFICATE";
NSString *const kDecisionBlockScope = @"BLOCK_SCOPE";
NSString *const kDecisionBlockTeamID = @"BLOCK_TEAMID";
NSString *const kDecisionBlockSigningID = @"BLOCK_SIGNINGID";
NSString *const kDecisionBlockCDHash = @"BLOCK_CDHASH";
NSString *const kDecisionUnknown = @"UNKNOWN";
NSString *const kDecisionBundleBinary = @"BUNDLE_BINARY";
NSString *const kLoggedInUsers = @"logged_in_users";
@@ -101,6 +105,7 @@ NSString *const kCertValidFrom = @"valid_from";
NSString *const kCertValidUntil = @"valid_until";
NSString *const kTeamID = @"team_id";
NSString *const kSigningID = @"signing_id";
NSString *const kCDHash = @"cdhash";
NSString *const kQuarantineDataURL = @"quarantine_data_url";
NSString *const kQuarantineRefererURL = @"quarantine_referer_url";
NSString *const kQuarantineTimestamp = @"quarantine_timestamp";
@@ -125,6 +130,7 @@ NSString *const kRuleTypeBinary = @"BINARY";
NSString *const kRuleTypeCertificate = @"CERTIFICATE";
NSString *const kRuleTypeTeamID = @"TEAMID";
NSString *const kRuleTypeSigningID = @"SIGNINGID";
NSString *const kRuleTypeCDHash = @"CDHASH";
NSString *const kRuleCustomMsg = @"custom_msg";
NSString *const kRuleCustomURL = @"custom_url";
NSString *const kCursor = @"cursor";

View File

@@ -12,6 +12,7 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/common/SNTRuleIdentifiers.h"
#import "Source/common/SNTXPCUnprivilegedControlInterface.h"
///
@@ -28,15 +29,12 @@
/// 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;
- (void)databaseRuleForBinarySHA256:(NSString *)binarySHA256
certificateSHA256:(NSString *)certificateSHA256
teamID:(NSString *)teamID
signingID:(NSString *)signingID
reply:(void (^)(SNTRule *))reply;
- (void)databaseRuleForIdentifiers:(SNTRuleIdentifiers *)identifiers
reply:(void (^)(SNTRule *))reply;
- (void)retrieveAllRules:(void (^)(NSArray<SNTRule *> *rules, NSError *error))reply;
///
@@ -45,7 +43,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

@@ -16,12 +16,23 @@
#import <MOLCertificate/MOLCertificate.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTRuleIdentifiers.h"
#import "Source/common/SantaVnode.h"
@class SNTRule;
@class SNTStoredEvent;
@class MOLXPCConnection;
struct RuleCounts {
int64_t binary;
int64_t certificate;
int64_t compiler;
int64_t transitive;
int64_t teamID;
int64_t signingID;
int64_t cdhash;
};
///
/// Protocol implemented by santad and utilized by santactl (unprivileged operations)
///
@@ -36,8 +47,7 @@
///
/// Database ops
///
- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate, int64_t compiler,
int64_t transitive, int64_t teamID, int64_t signingID))reply;
- (void)databaseRuleCounts:(void (^)(struct RuleCounts ruleCounts))reply;
- (void)databaseEventCount:(void (^)(int64_t count))reply;
- (void)staticRuleCount:(void (^)(int64_t count))reply;
@@ -47,17 +57,10 @@
///
/// @param filePath A Path to the file, can be nil.
/// @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 A SHA256 hash of the signing certificate, can be nil.
/// @note If fileInfo and signingCertificate are both passed in, the most specific rule will be
/// returned. Binary rules take precedence over cert rules.
/// @param identifiers The various identifiers to be used when making a decision.
///
- (void)decisionForFilePath:(NSString *)filePath
fileSHA256:(NSString *)fileSHA256
certificateSHA256:(NSString *)certificateSHA256
teamID:(NSString *)teamID
signingID:(NSString *)signingID
identifiers:(SNTRuleIdentifiers *)identifiers
reply:(void (^)(SNTEventState))reply;
///
@@ -68,9 +71,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

@@ -320,8 +320,8 @@ class SantaCache {
Lock a bucket. Spins until the lock is acquired.
*/
inline void lock(struct bucket *bucket) const {
while (OSAtomicTestAndSet(7, (volatile uint8_t *)&bucket->head))
;
while (OSAtomicTestAndSet(7, (volatile uint8_t *)&bucket->head)) {
}
}
/**

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

@@ -4,6 +4,7 @@ syntax = "proto3";
import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";
import "Source/santad/ProcessTree/process_tree.proto";
option objc_class_prefix = "SNTPB";
@@ -173,6 +174,8 @@ message ProcessInfo {
// Time the process was started
optional google.protobuf.Timestamp start_time = 17;
optional process_tree.Annotations annotations = 18;
}
// Light variant of ProcessInfo message to help minimize on-disk/on-wire sizes
@@ -202,6 +205,8 @@ message ProcessInfoLight {
// File information for the executable backing this process
optional FileInfoLight executable = 10;
optional process_tree.Annotations annotations = 11;
}
// Certificate information
@@ -213,6 +218,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
@@ -263,6 +289,7 @@ message Execution {
REASON_LONG_PATH = 9;
REASON_NOT_RUNNING = 10;
REASON_SIGNING_ID = 11;
REASON_CDHASH = 12;
}
optional Reason reason = 10;
@@ -286,6 +313,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 +411,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 +564,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",
@@ -67,6 +68,7 @@ objc_library(
"//Source/common:SNTLogging",
"//Source/common:SNTMetricSet",
"//Source/common:SNTRule",
"//Source/common:SNTRuleIdentifiers",
"//Source/common:SNTStoredEvent",
"//Source/common:SNTStrengthify",
"//Source/common:SNTSystemInfo",
@@ -115,6 +117,9 @@ santa_unit_test(
"//Source/common:SNTFileInfo",
"//Source/common:SNTLogging",
"//Source/common:SNTRule",
"//Source/common:SNTRuleIdentifiers",
"//Source/common:SNTStoredEvent",
"//Source/common:SNTXPCBundleServiceInterface",
"//Source/common:SNTXPCControlInterface",
"@MOLCertificate",
"@MOLCodesignChecker",
@@ -145,11 +150,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,9 @@
#import "Source/common/SNTFileInfo.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTRule.h"
#import "Source/common/SNTRuleIdentifiers.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"
@@ -43,6 +46,7 @@ static NSString *const kSigningChain = @"Signing Chain";
static NSString *const kUniversalSigningChain = @"Universal Signing Chain";
static NSString *const kTeamID = @"Team ID";
static NSString *const kSigningID = @"Signing ID";
static NSString *const kCDHash = @"CDHash";
// signing chain keys
static NSString *const kCommonName = @"Common Name";
@@ -55,6 +59,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 +83,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;
@@ -113,6 +125,7 @@ typedef id (^SNTAttributeBlock)(SNTCommandFileInfo *, SNTFileInfo *);
@property(readonly, copy, nonatomic) SNTAttributeBlock downloadAgent;
@property(readonly, copy, nonatomic) SNTAttributeBlock teamID;
@property(readonly, copy, nonatomic) SNTAttributeBlock signingID;
@property(readonly, copy, nonatomic) SNTAttributeBlock cdhash;
@property(readonly, copy, nonatomic) SNTAttributeBlock type;
@property(readonly, copy, nonatomic) SNTAttributeBlock pageZero;
@property(readonly, copy, nonatomic) SNTAttributeBlock codeSigned;
@@ -156,6 +169,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 +181,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"
@@ -186,8 +204,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
+ (NSArray<NSString *> *)fileInfoKeys {
return @[
kPath, kSHA256, kSHA1, kBundleName, kBundleVersion, kBundleVersionStr, kDownloadReferrerURL,
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kSigningID, kType, kPageZero,
kCodeSigned, kRule, kSigningChain, kUniversalSigningChain
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kSigningID, kCDHash, kType,
kPageZero, kCodeSigned, kRule, kSigningChain, kUniversalSigningChain
];
}
@@ -221,6 +239,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
kUniversalSigningChain : self.universalSigningChain,
kTeamID : self.teamID,
kSigningID : self.signingID,
kCDHash : self.cdhash,
};
_printQueue = dispatch_queue_create("com.google.santactl.print_queue", DISPATCH_QUEUE_SERIAL);
@@ -361,6 +380,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
NSError *err;
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:&err];
NSString *cdhash =
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoUnique];
NSString *teamID =
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier];
NSString *identifier =
@@ -379,15 +400,21 @@ REGISTER_COMMAND_NAME(@"fileinfo")
}
}
[[cmd.daemonConn remoteObjectProxy] decisionForFilePath:fileInfo.path
fileSHA256:fileInfo.SHA256
certificateSHA256:err ? nil : csc.leafCertificate.SHA256
teamID:teamID
signingID:signingID
reply:^(SNTEventState s) {
state = s;
dispatch_semaphore_signal(sema);
}];
struct RuleIdentifiers identifiers = {
.cdhash = cdhash,
.binarySHA256 = fileInfo.SHA256,
.signingID = signingID,
.certificateSHA256 = err ? nil : csc.leafCertificate.SHA256,
.teamID = teamID,
};
[[cmd.daemonConn remoteObjectProxy]
decisionForFilePath:fileInfo.path
identifiers:[[SNTRuleIdentifiers alloc] initWithRuleIdentifiers:identifiers]
reply:^(SNTEventState s) {
state = s;
dispatch_semaphore_signal(sema);
}];
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
cmd.daemonUnavailable = YES;
return kCommunicationErrorMsg;
@@ -405,6 +432,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
case SNTEventStateAllowSigningID:
case SNTEventStateBlockSigningID: [output appendString:@" (SigningID)"]; break;
case SNTEventStateAllowCDHash:
case SNTEventStateBlockCDHash: [output appendString:@" (CDHash)"]; break;
case SNTEventStateAllowScope:
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
case SNTEventStateAllowCompiler: [output appendString:@" (Compiler)"]; break;
@@ -504,6 +533,13 @@ REGISTER_COMMAND_NAME(@"fileinfo")
};
}
- (SNTAttributeBlock)cdhash {
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
return [csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoUnique];
};
}
#pragma mark -
// Entry point for the command.
@@ -682,6 +718,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 +788,11 @@ REGISTER_COMMAND_NAME(@"fileinfo")
}
}
}
if (self.bundleInfo) {
[output appendString:[self stringForBundleInfo:outputDict[kBundleInfo] key:kBundleInfo]];
}
if (!singleKey) [output appendString:@"\n"];
}
@@ -739,6 +822,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 +874,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 +964,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

@@ -12,7 +12,9 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <CommonCrypto/CommonDigest.h>
#import <Foundation/Foundation.h>
#import <Kernel/kern/cs_blobs.h>
#import <MOLCertificate/MOLCertificate.h>
#import <MOLCodesignChecker/MOLCodesignChecker.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
@@ -22,13 +24,12 @@
#import "Source/common/SNTFileInfo.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTRule.h"
#import "Source/common/SNTRuleIdentifiers.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")
@@ -46,59 +47,66 @@ REGISTER_COMMAND_NAME(@"rule")
}
+ (NSString *)longHelpText {
return (@"Usage: santactl rule [options]\n"
@" One of:\n"
@" --allow: add to allow\n"
@" --block: add to block\n"
@" --silent-block: add to silent block\n"
@" --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"
@"\n"
@" One of:\n"
@" --path {path}: path of binary/bundle to add/remove.\n"
@" Will add the hash of the file currently at that path.\n"
@" Does not work with --check. Use the fileinfo verb to check.\n"
@" 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"
@" --signingid: add or check a signing ID rule instead of binary (see notes)\n"
@" --certificate: add or check a certificate sha256 rule instead of binary\n"
return (
@"Usage: santactl rule [options]\n"
@" One of:\n"
@" --allow: add to allow\n"
@" --block: add to block\n"
@" --silent-block: add to silent block\n"
@" --compiler: allow and mark as a compiler\n"
@" --remove: remove existing rule\n"
@" --check: check for an existing rule\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"
@" Will add the hash of the file currently at that path.\n"
@" Does not work with --check. Use the fileinfo verb to check.\n"
@" the rule state of a file.\n"
@" --identifier {sha256|teamID|signingID|cdhash}: identifier to add/remove/check\n"
@" --sha256 {sha256}: hash to add/remove/check [deprecated]\n"
@"\n"
@" Optionally:\n"
@" --teamid: add or check a team ID rule instead of binary\n"
@" --signingid: add or check a signing ID rule instead of binary (see notes)\n"
@" --certificate: add or check a certificate sha256 rule instead of binary\n"
@" --cdhash: add or check a cdhash rule instead of binary\n"
#ifdef DEBUG
@" --force: allow manual changes even when SyncBaseUrl is set\n"
@" --force: allow manual changes even when SyncBaseUrl is set\n"
#endif
@" --message {message}: custom message\n"
@"\n"
@" Notes:\n"
@" The format of `identifier` when adding/checking a `signingid` rule is:\n"
@"\n"
@" `TeamID:SigningID`\n"
@"\n"
@" Because signing IDs are controlled by the binary author, this ensures\n"
@" that the signing ID is properly scoped to a developer. For the special\n"
@" case of platform binaries, `TeamID` should be replaced with the string\n"
@" \"platform\" (e.g. `platform:SigningID`). This allows for rules\n"
@" targeting Apple-signed binaries that do not have a team ID.\n"
@"\n"
@" Importing / Exporting Rules:\n"
@" If santa is not configured to use a sync server one can export\n"
@" & import its non-static rules to and from JSON files using the \n"
@" --export/--import flags. These files have the following form:\n"
@"\n"
@" {\"rules\": [{rule-dictionaries}]}\n"
@" e.g. {\"rules\": [\n"
@" {\"policy\": \"BLOCKLIST\",\n"
@" \"identifier\": "
@"\"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6\"\n"
@" \"custom_url\" : \"\",\n"
@" \"custom_msg\": \"/bin/ls block for demo\"}\n"
@" ]}\n");
@" --message {message}: custom message\n"
@" --clean: when importing rules via JSON clear all non-transitive rules before importing\n"
@" --clean-all: when importing rules via JSON clear all rules before importing\n"
@"\n"
@" Notes:\n"
@" The format of `identifier` when adding/checking a `signingid` rule is:\n"
@"\n"
@" `TeamID:SigningID`\n"
@"\n"
@" Because signing IDs are controlled by the binary author, this ensures\n"
@" that the signing ID is properly scoped to a developer. For the special\n"
@" case of platform binaries, `TeamID` should be replaced with the string\n"
@" \"platform\" (e.g. `platform:SigningID`). This allows for rules\n"
@" targeting Apple-signed binaries that do not have a team ID.\n"
@"\n"
@" Importing / Exporting Rules:\n"
@" If santa is not configured to use a sync server one can export\n"
@" & import its non-static rules to and from JSON files using the \n"
@" --export/--import flags. These files have the following form:\n"
@"\n"
@" {\"rules\": [{rule-dictionaries}]}\n"
@" e.g. {\"rules\": [\n"
@" {\"policy\": \"BLOCKLIST\",\n"
@" \"identifier\": "
@"\"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6\"\n"
@" \"custom_url\" : \"\",\n"
@" \"custom_msg\": \"/bin/ls block for demo\"}\n"
@" ]}\n"
@"\n"
@" By default rules are not cleared when importing. To clear the\n"
@" database you must use either --clean or --clean-all\n"
@"\n");
}
- (void)runWithArguments:(NSArray *)arguments {
@@ -122,6 +130,7 @@ REGISTER_COMMAND_NAME(@"rule")
NSString *path;
NSString *jsonFilePath;
BOOL check = NO;
SNTRuleCleanup cleanupType = SNTRuleCleanupNone;
BOOL importRules = NO;
BOOL exportRules = NO;
@@ -150,6 +159,8 @@ REGISTER_COMMAND_NAME(@"rule")
newRule.type = SNTRuleTypeTeamID;
} else if ([arg caseInsensitiveCompare:@"--signingid"] == NSOrderedSame) {
newRule.type = SNTRuleTypeSigningID;
} else if ([arg caseInsensitiveCompare:@"--cdhash"] == NSOrderedSame) {
newRule.type = SNTRuleTypeCDHash;
} else if ([arg caseInsensitiveCompare:@"--path"] == NSOrderedSame) {
if (++i > arguments.count - 1) {
[self printErrorUsageAndExit:@"--path requires an argument"];
@@ -174,11 +185,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"];
@@ -188,6 +194,10 @@ REGISTER_COMMAND_NAME(@"rule")
[self printErrorUsageAndExit:@"--import requires an argument"];
}
jsonFilePath = arguments[i];
} else if ([arg caseInsensitiveCompare:@"--clean"] == NSOrderedSame) {
cleanupType = SNTRuleCleanupNonTransitive;
} else if ([arg caseInsensitiveCompare:@"--clean-all"] == NSOrderedSame) {
cleanupType = SNTRuleCleanupAll;
} else if ([arg caseInsensitiveCompare:@"--export"] == NSOrderedSame) {
if (importRules) {
[self printErrorUsageAndExit:@"--import and --export are mutually exclusive"];
@@ -206,6 +216,36 @@ REGISTER_COMMAND_NAME(@"rule")
}
}
if (!importRules && cleanupType != SNTRuleCleanupNone) {
switch (cleanupType) {
case SNTRuleCleanupNonTransitive:
[self printErrorUsageAndExit:@"--clean can only be used with --import"];
break;
case SNTRuleCleanupAll:
[self printErrorUsageAndExit:@"--clean-all can only be used with --import"];
break;
default:
// This is a programming error.
LOGE(@"Unexpected SNTRuleCleanupType %ld", cleanupType);
exit(EXIT_FAILURE);
}
}
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 with:cleanupType];
} 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) {
@@ -217,17 +257,29 @@ REGISTER_COMMAND_NAME(@"rule")
} else if (newRule.type == SNTRuleTypeCertificate) {
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
newRule.identifier = cs.leafCertificate.SHA256;
} else if (newRule.type == SNTRuleTypeCDHash) {
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
newRule.identifier =
[cs.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
} else if (newRule.type == SNTRuleTypeTeamID || newRule.type == SNTRuleTypeSigningID) {
// noop
}
}
if (newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate) {
if (newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate ||
newRule.type == SNTRuleTypeCDHash) {
NSCharacterSet *nonHex =
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
if ([[newRule.identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length !=
64) {
NSUInteger length =
[[newRule.identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length;
if ((newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate) &&
length != CC_SHA256_DIGEST_LENGTH * 2) {
[self printErrorUsageAndExit:@"BINARY or CERTIFICATE rules require a valid SHA-256"];
} else if (newRule.type == SNTRuleTypeCDHash && length != CS_CDHASH_LEN * 2) {
[self printErrorUsageAndExit:
[NSString stringWithFormat:@"CDHASH rules require a valid hex string of length %d",
CS_CDHASH_LEN * 2]];
}
}
@@ -236,16 +288,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 +296,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",
@@ -264,7 +306,7 @@ REGISTER_COMMAND_NAME(@"rule")
} else {
NSString *ruleType;
switch (newRule.type) {
case SNTRuleTypeCertificate:
case SNTRuleTypeCertificate: ruleType = @"Certificate SHA-256"; break;
case SNTRuleTypeBinary: {
ruleType = @"SHA-256";
break;
@@ -273,6 +315,14 @@ REGISTER_COMMAND_NAME(@"rule")
ruleType = @"Team ID";
break;
}
case SNTRuleTypeSigningID: {
ruleType = @"Signing ID";
break;
}
case SNTRuleTypeCDHash: {
ruleType = @"CDHash";
break;
}
default: ruleType = @"(Unknown type)";
}
if (newRule.state == SNTRuleStateRemove) {
@@ -287,79 +337,109 @@ 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 SNTRuleTypeCDHash: [output appendString:@"CDHash"]; 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]]];
}
}];
struct RuleIdentifiers identifiers = {
.cdhash = (rule.type == SNTRuleTypeCDHash) ? rule.identifier : nil,
.binarySHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil,
.certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil,
.teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil,
.signingID = (rule.type == SNTRuleTypeSigningID) ? rule.identifier : nil,
};
[rop databaseRuleForIdentifiers:[[SNTRuleIdentifiers alloc] initWithRuleIdentifiers:identifiers]
reply:^(SNTRule *r) {
output = [SNTCommandRule stringifyRule:r
withColor:(isatty(STDOUT_FILENO) == 1)];
}];
printf("%s\n", output.UTF8String);
exit(0);
}
- (void)importJSONFile:(NSString *)jsonFilePath {
- (void)importJSONFile:(NSString *)jsonFilePath with:(SNTRuleCleanup)cleanupType {
// If the file exists parse it and then add the rules one at a time.
NSError *error;
NSData *data = [NSData dataWithContentsOfFile:jsonFilePath options:0 error:&error];
@@ -396,7 +476,7 @@ REGISTER_COMMAND_NAME(@"rule")
[[self.daemonConn remoteObjectProxy]
databaseRuleAddRules:parsedRules
cleanSlate:NO
ruleCleanup:cleanupType
reply:^(NSError *error) {
if (error) {
printf("Failed to modify rules: %s",
@@ -426,7 +506,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
@@ -81,22 +91,20 @@ REGISTER_COMMAND_NAME(@"status")
}];
// Database counts
__block int64_t eventCount = -1;
__block int64_t binaryRuleCount = -1;
__block int64_t certRuleCount = -1;
__block int64_t teamIDRuleCount = -1;
__block int64_t signingIDRuleCount = -1;
__block int64_t compilerRuleCount = -1;
__block int64_t transitiveRuleCount = -1;
[rop databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler,
int64_t transitive, int64_t teamID, int64_t signingID) {
binaryRuleCount = binary;
certRuleCount = certificate;
teamIDRuleCount = teamID;
signingIDRuleCount = signingID;
compilerRuleCount = compiler;
transitiveRuleCount = transitive;
__block struct RuleCounts ruleCounts = {
.binary = -1,
.certificate = -1,
.compiler = -1,
.transitive = -1,
.teamID = -1,
.signingID = -1,
.cdhash = -1,
};
[rop databaseRuleCounts:^(struct RuleCounts counts) {
ruleCounts = counts;
}];
__block int64_t eventCount = -1;
[rop databaseEventCount:^(int64_t count) {
eventCount = count;
}];
@@ -119,8 +127,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 +166,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,24 +198,25 @@ 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),
@"certificate_rules" : @(certRuleCount),
@"teamid_rules" : @(teamIDRuleCount),
@"signingid_rules" : @(signingIDRuleCount),
@"compiler_rules" : @(compilerRuleCount),
@"transitive_rules" : @(transitiveRuleCount),
@"binary_rules" : @(ruleCounts.binary),
@"certificate_rules" : @(ruleCounts.certificate),
@"teamid_rules" : @(ruleCounts.teamID),
@"signingid_rules" : @(ruleCounts.signingID),
@"cdhash_rules" : @(ruleCounts.cdhash),
@"compiler_rules" : @(ruleCounts.compiler),
@"transitive_rules" : @(ruleCounts.transitive),
@"events_pending_upload" : @(eventCount),
},
@"static_rules" : @{
@@ -215,7 +229,6 @@ REGISTER_COMMAND_NAME(@"status")
@"last_successful_rule" : ruleSyncLastSuccessStr ?: @"null",
@"push_notifications" : pushNotifications ? @"Connected" : @"Disconnected",
@"bundle_scanning" : @(enableBundles),
@"transitive_rules" : @(enableTransitiveRules),
},
} mutableCopy];
@@ -248,13 +261,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);
@@ -263,12 +283,13 @@ REGISTER_COMMAND_NAME(@"status")
printf(" %-25s | %lld\n", "Non-root cache count", nonRootCacheCount);
printf(">>> Database Info\n");
printf(" %-25s | %lld\n", "Binary Rules", binaryRuleCount);
printf(" %-25s | %lld\n", "Certificate Rules", certRuleCount);
printf(" %-25s | %lld\n", "TeamID Rules", teamIDRuleCount);
printf(" %-25s | %lld\n", "SigningID Rules", signingIDRuleCount);
printf(" %-25s | %lld\n", "Compiler Rules", compilerRuleCount);
printf(" %-25s | %lld\n", "Transitive Rules", transitiveRuleCount);
printf(" %-25s | %lld\n", "Binary Rules", ruleCounts.binary);
printf(" %-25s | %lld\n", "Certificate Rules", ruleCounts.certificate);
printf(" %-25s | %lld\n", "TeamID Rules", ruleCounts.teamID);
printf(" %-25s | %lld\n", "SigningID Rules", ruleCounts.signingID);
printf(" %-25s | %lld\n", "CDHash Rules", ruleCounts.cdhash);
printf(" %-25s | %lld\n", "Compiler Rules", ruleCounts.compiler);
printf(" %-25s | %lld\n", "Transitive Rules", ruleCounts.transitive);
printf(" %-25s | %lld\n", "Events Pending Upload", eventCount);
if ([SNTConfigurator configurator].staticRules.count) {
@@ -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",
@@ -30,6 +33,7 @@ objc_library(
"//Source/common:SNTFileInfo",
"//Source/common:SNTLogging",
"//Source/common:SNTRule",
"//Source/common:SNTRuleIdentifiers",
"@MOLCertificate",
"@MOLCodesignChecker",
],
@@ -189,19 +193,18 @@ objc_library(
objc_library(
name = "SNTPolicyProcessor",
srcs = [
"DataLayer/SNTDatabaseTable.h",
"DataLayer/SNTRuleTable.h",
"SNTPolicyProcessor.m",
],
srcs = ["SNTPolicyProcessor.m"],
hdrs = ["SNTPolicyProcessor.h"],
deps = [
":SNTRuleTable",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTDeepCopy",
"//Source/common:SNTFileInfo",
"//Source/common:SNTLogging",
"//Source/common:SNTRule",
"//Source/common:SNTRuleIdentifiers",
"@FMDB",
"@MOLCertificate",
"@MOLCodesignChecker",
@@ -236,10 +239,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 +253,9 @@ objc_library(
"//Source/common:SNTStoredEvent",
"//Source/common:SantaVnode",
"//Source/common:String",
"//Source/common:Unit",
"@MOLCodesignChecker",
"@com_google_absl//absl/synchronization",
],
)
@@ -278,12 +285,27 @@ objc_library(
":SNTEndpointSecurityClientBase",
":WatchItemPolicy",
"//Source/common:BranchPrediction",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTLogging",
"//Source/common:SystemResources",
],
)
objc_library(
name = "SNTEndpointSecurityTreeAwareClient",
srcs = ["EventProviders/SNTEndpointSecurityTreeAwareClient.mm"],
hdrs = ["EventProviders/SNTEndpointSecurityTreeAwareClient.h"],
deps = [
":EndpointSecurityAPI",
":EndpointSecurityMessage",
":Metrics",
":SNTEndpointSecurityClient",
"//Source/santad/ProcessTree:SNTEndpointSecurityAdapter",
"//Source/santad/ProcessTree:process_tree",
],
)
objc_library(
name = "SNTEndpointSecurityRecorder",
srcs = ["EventProviders/SNTEndpointSecurityRecorder.mm"],
@@ -297,13 +319,14 @@ objc_library(
":EndpointSecurityMessage",
":Metrics",
":SNTCompilerController",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
":SNTEndpointSecurityTreeAwareClient",
"//Source/common:PrefixTree",
"//Source/common:SNTConfigurator",
"//Source/common:SNTLogging",
"//Source/common:String",
"//Source/common:Unit",
"//Source/santad/ProcessTree:process_tree",
],
)
@@ -395,8 +418,10 @@ objc_library(
":Metrics",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTDeviceEvent",
"//Source/common:SNTLogging",
"//Source/common:SNTMetricSet",
],
)
@@ -435,6 +460,8 @@ objc_library(
":EndpointSecurityEnrichedTypes",
"//Source/common:SNTLogging",
"//Source/common:SantaCache",
"//Source/santad/ProcessTree:SNTEndpointSecurityAdapter",
"//Source/santad/ProcessTree:process_tree",
],
)
@@ -443,6 +470,7 @@ objc_library(
hdrs = ["EventProviders/EndpointSecurity/EnrichedTypes.h"],
deps = [
":EndpointSecurityMessage",
"//Source/santad/ProcessTree:process_tree_cc_proto",
],
)
@@ -468,7 +496,6 @@ objc_library(
"bsm",
],
deps = [
":EndpointSecurityEnrichedTypes",
":EndpointSecurityMessage",
":SNTDecisionCache",
"//Source/common:SantaCache",
@@ -618,6 +645,7 @@ objc_library(
deps = [
":EndpointSecurityClient",
":WatchItemPolicy",
"//Source/santad/ProcessTree:process_tree",
],
)
@@ -630,6 +658,9 @@ objc_library(
name = "EndpointSecurityAPI",
srcs = ["EventProviders/EndpointSecurity/EndpointSecurityAPI.mm"],
hdrs = ["EventProviders/EndpointSecurity/EndpointSecurityAPI.h"],
sdk_dylibs = [
"EndpointSecurity",
],
deps = [
":EndpointSecurityClient",
":EndpointSecurityMessage",
@@ -658,6 +689,7 @@ objc_library(
"//Source/common:SNTLogging",
"//Source/common:SNTMetricSet",
"//Source/common:SNTRule",
"//Source/common:SNTRuleIdentifiers",
"//Source/common:SNTStoredEvent",
"//Source/common:SNTStrengthify",
"//Source/common:SNTXPCControlInterface",
@@ -714,6 +746,7 @@ objc_library(
"//Source/common:SNTXPCNotifierInterface",
"//Source/common:SNTXPCSyncServiceInterface",
"//Source/common:Unit",
"//Source/santad/ProcessTree:process_tree",
"@MOLXPCConnection",
],
)
@@ -745,6 +778,7 @@ objc_library(
"//Source/common:SNTXPCControlInterface",
"//Source/common:SNTXPCUnprivilegedControlInterface",
"//Source/common:Unit",
"//Source/santad/ProcessTree:process_tree",
"@MOLXPCConnection",
],
)
@@ -867,9 +901,11 @@ santa_unit_test(
"//Source/common:SNTFileInfo",
"//Source/common:SNTLogging",
"//Source/common:SNTRule",
"//Source/common:SNTRuleIdentifiers",
"@FMDB",
"@MOLCertificate",
"@MOLCodesignChecker",
"@OCMock",
],
)
@@ -895,6 +931,7 @@ santa_unit_test(
":SNTDatabaseController",
":SNTDecisionCache",
":SNTEndpointSecurityAuthorizer",
":SNTEndpointSecurityClient",
":SantadDeps",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTConfigurator",
@@ -1152,6 +1189,7 @@ santa_unit_test(
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTRule",
"//Source/common:SantaVnode",
"//Source/common:TestUtils",
"@OCMock",
],
@@ -1172,7 +1210,9 @@ santa_unit_test(
":MockEndpointSecurityAPI",
":SNTEndpointSecurityClient",
":WatchItemPolicy",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SystemResources",
"//Source/common:TestUtils",
"@OCMock",
"@com_google_googletest//:gtest",
@@ -1200,6 +1240,7 @@ santa_unit_test(
"//Source/common:SNTFileInfo",
"//Source/common:SNTMetricSet",
"//Source/common:SNTRule",
"//Source/common:SNTRuleIdentifiers",
"//Source/common:TestUtils",
"@MOLCertificate",
"@MOLCodesignChecker",
@@ -1316,7 +1357,9 @@ santa_unit_test(
":EndpointSecurityMessage",
":Metrics",
":MockEndpointSecurityAPI",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityDeviceManager",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTDeviceEvent",
"//Source/common:TestUtils",

View File

@@ -15,6 +15,7 @@
#import <Foundation/Foundation.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTRuleIdentifiers.h"
#import "Source/santad/DataLayer/SNTDatabaseTable.h"
@class SNTCachedDecision;
@@ -29,57 +30,60 @@
///
/// @return Number of rules in the database
///
- (NSUInteger)ruleCount;
- (int64_t)ruleCount;
///
/// @return Number of binary rules in the database
///
- (NSUInteger)binaryRuleCount;
- (int64_t)binaryRuleCount;
///
/// @return Number of compiler rules in the database
///
- (NSUInteger)compilerRuleCount;
- (int64_t)compilerRuleCount;
///
/// @return Number of transitive rules in the database
///
- (NSUInteger)transitiveRuleCount;
- (int64_t)transitiveRuleCount;
///
/// @return Number of certificate rules in the database
///
- (NSUInteger)certificateRuleCount;
- (int64_t)certificateRuleCount;
///
/// @return Number of team ID rules in the database
///
- (NSUInteger)teamIDRuleCount;
- (int64_t)teamIDRuleCount;
///
/// @return Number of signing ID rules in the database
///
- (NSUInteger)signingIDRuleCount;
- (int64_t)signingIDRuleCount;
///
/// @return Rule for binary, signingID, certificate or teamID (in that order).
/// @return Number of cdhash rules in the database
///
- (int64_t)cdhashRuleCount;
///
/// @return Rule for given identifiers.
/// Currently: binary, signingID, certificate or teamID (in that order).
/// The first matching rule found is returned.
///
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
signingID:(NSString *)signingID
certificateSHA256:(NSString *)certificateSHA256
teamID:(NSString *)teamID;
- (SNTRule *)ruleForIdentifiers:(struct RuleIdentifiers)identifiers;
///
/// Add an array of rules to the database. The rules will be added within a transaction and the
/// 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

@@ -29,7 +29,7 @@ static const uint32_t kRuleTableCurrentVersion = 7;
// TODO(nguyenphillip): this should be configurable.
// How many rules must be in database before we start trying to remove transitive rules.
static const NSUInteger kTransitiveRuleCullingThreshold = 500000;
static const int64_t kTransitiveRuleCullingThreshold = 500000;
// Consider transitive rules out of date if they haven't been used in six months.
static const NSUInteger kTransitiveRuleExpirationSeconds = 6 * 30 * 24 * 3600;
@@ -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;
}
@@ -263,7 +263,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
#pragma mark Entry Counts
- (NSUInteger)ruleCount {
- (int64_t)ruleCount {
__block NSUInteger count = 0;
[self inDatabase:^(FMDatabase *db) {
count = [db longForQuery:@"SELECT COUNT(*) FROM rules"];
@@ -271,23 +271,23 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
return count;
}
- (NSUInteger)ruleCountForRuleType:(SNTRuleType)ruleType {
__block NSUInteger count = 0;
- (int64_t)ruleCountForRuleType:(SNTRuleType)ruleType {
__block int64_t count = 0;
[self inDatabase:^(FMDatabase *db) {
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=?", @(ruleType)];
}];
return count;
}
- (NSUInteger)binaryRuleCount {
- (int64_t)binaryRuleCount {
return [self ruleCountForRuleType:SNTRuleTypeBinary];
}
- (NSUInteger)certificateRuleCount {
- (int64_t)certificateRuleCount {
return [self ruleCountForRuleType:SNTRuleTypeCertificate];
}
- (NSUInteger)compilerRuleCount {
- (int64_t)compilerRuleCount {
__block NSUInteger count = 0;
[self inDatabase:^(FMDatabase *db) {
count =
@@ -296,7 +296,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
return count;
}
- (NSUInteger)transitiveRuleCount {
- (int64_t)transitiveRuleCount {
__block NSUInteger count = 0;
[self inDatabase:^(FMDatabase *db) {
count =
@@ -305,14 +305,18 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
return count;
}
- (NSUInteger)teamIDRuleCount {
- (int64_t)teamIDRuleCount {
return [self ruleCountForRuleType:SNTRuleTypeTeamID];
}
- (NSUInteger)signingIDRuleCount {
- (int64_t)signingIDRuleCount {
return [self ruleCountForRuleType:SNTRuleTypeSigningID];
}
- (int64_t)cdhashRuleCount {
return [self ruleCountForRuleType:SNTRuleTypeCDHash];
}
- (SNTRule *)ruleFromResultSet:(FMResultSet *)rs {
SNTRule *r = [[SNTRule alloc] initWithIdentifier:[rs stringForColumn:@"identifier"]
state:[rs intForColumn:@"state"]
@@ -323,10 +327,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
return r;
}
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
signingID:(NSString *)signingID
certificateSHA256:(NSString *)certificateSHA256
teamID:(NSString *)teamID {
- (SNTRule *)ruleForIdentifiers:(struct RuleIdentifiers)identifiers {
__block SNTRule *rule;
// Look for a static rule that matches.
@@ -334,22 +335,27 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
if (staticRules.count) {
// IMPORTANT: The order static rules are checked here should be the same
// order as given by the SQL query for the rules database.
rule = staticRules[binarySHA256];
rule = staticRules[identifiers.cdhash];
if (rule.type == SNTRuleTypeCDHash) {
return rule;
}
rule = staticRules[identifiers.binarySHA256];
if (rule.type == SNTRuleTypeBinary) {
return rule;
}
rule = staticRules[signingID];
rule = staticRules[identifiers.signingID];
if (rule.type == SNTRuleTypeSigningID) {
return rule;
}
rule = staticRules[certificateSHA256];
rule = staticRules[identifiers.certificateSHA256];
if (rule.type == SNTRuleTypeCertificate) {
return rule;
}
rule = staticRules[teamID];
rule = staticRules[identifiers.teamID];
if (rule.type == SNTRuleTypeTeamID) {
return rule;
}
@@ -360,9 +366,9 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
// NOTE: This code is written with the intention that the binary rule is searched for first
// as Santa is designed to go with the most-specific rule possible.
//
// The intended order of precedence is Binaries > Signing IDs > Certificates > Team IDs.
// The intended order of precedence is CDHash > Binaries > Signing IDs > Certificates > Team IDs.
//
// As such the query should have "ORDER BY type DESC" before the LIMIT, to ensure that is the
// As such the query should have "ORDER BY type ASC" before the LIMIT, to ensure that is the
// case. However, in all tested versions of SQLite that ORDER BY clause is unnecessary: the query
// is performed 'as written' by doing separate lookups in the index and the later lookups are if
// the first returns a result. That behavior can be checked here: http://sqlfiddle.com/#!5/cdc42/1
@@ -375,12 +381,15 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
// There is a test for this in SNTRuleTableTests in case SQLite behavior changes in the future.
//
[self inDatabase:^(FMDatabase *db) {
FMResultSet *rs = [db executeQuery:@"SELECT * FROM rules WHERE "
@" (identifier=? and type=1000) "
@"OR (identifier=? AND type=2000) "
@"OR (identifier=? AND type=3000) "
@"OR (identifier=? AND type=4000) LIMIT 1",
binarySHA256, signingID, certificateSHA256, teamID];
FMResultSet *rs =
[db executeQuery:@"SELECT * FROM rules WHERE "
@" (identifier=? AND type=500) "
@"OR (identifier=? AND type=1000) "
@"OR (identifier=? AND type=2000) "
@"OR (identifier=? AND type=3000) "
@"OR (identifier=? AND type=4000) LIMIT 1",
identifiers.cdhash, identifiers.binarySHA256, identifiers.signingID,
identifiers.certificateSHA256, identifiers.teamID];
if ([rs next]) {
rule = [self ruleFromResultSet:rs];
}
@@ -389,8 +398,8 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
// Allow binaries signed by the "Software Signing" cert used to sign launchd
// if no existing rule has matched.
if (!rule && [certificateSHA256 isEqual:self.launchdCSInfo.leafCertificate.SHA256]) {
rule = [[SNTRule alloc] initWithIdentifier:certificateSHA256
if (!rule && [identifiers.certificateSHA256 isEqual:self.launchdCSInfo.leafCertificate.SHA256]) {
rule = [[SNTRule alloc] initWithIdentifier:identifiers.certificateSHA256
state:SNTRuleStateAllow
type:SNTRuleTypeCertificate
customMsg:nil
@@ -403,7 +412,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 +422,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) {
@@ -452,25 +463,59 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
}
- (BOOL)addedRulesShouldFlushDecisionCache:(NSArray *)rules {
// Check for non-plain-allowlist rules first before querying the database.
uint64_t nonAllowRuleCount = 0;
for (SNTRule *rule in rules) {
if (rule.state != SNTRuleStateAllow) return YES;
// If the rule is a remove rule, act conservatively and flush the cache.
// This is to make sure cached rules of different precedence rules do not
// impact final decision.
if (rule.state == SNTRuleStateRemove) {
return YES;
}
if (rule.state != SNTRuleStateAllow) {
nonAllowRuleCount++;
// Just flush if we more than 1000 block rules.
if (nonAllowRuleCount >= 1000) return YES;
}
}
// If still here, then all rules in the array are allowlist rules. So now we look for allowlist
// rules where there is a previously existing allowlist compiler rule for the same identifier.
// If so we find such a rule, then cache should be flushed.
// Check newly synced rules for any blocking rules. If any are found, check
// in the db to see if they already exist. If they're not found or were
// previously allow rules flush the cache.
//
// If all rules in the array are allowlist rules, look for allowlist rules
// where there is a previously existing allowlist compiler rule for the same
// identifier. If so we find such a rule, then cache should be flushed.
__block BOOL flushDecisionCache = NO;
[self inTransaction:^(FMDatabase *db, BOOL *rollback) {
for (SNTRule *rule in rules) {
// Allowlist certificate rules are ignored
if (rule.type == SNTRuleTypeCertificate) continue;
// If the rule is a block rule, silent block rule, or a compiler rule check if it already
// exists in the database.
//
// If it does not then flush the cache. To ensure that the new rule is honored.
if ((rule.state != SNTRuleStateAllow)) {
if ([db longForQuery:
@"SELECT COUNT(*) FROM rules WHERE identifier=? AND type=? AND state=? LIMIT 1",
rule.identifier, @(rule.type), @(rule.state)] == 0) {
flushDecisionCache = YES;
return;
}
} else {
// At this point we know the rule is an allowlist rule. Check if it's
// overriding a compiler rule.
if ([db longForQuery:
@"SELECT COUNT(*) FROM rules WHERE identifier=? AND type=? AND state=? LIMIT 1",
rule.identifier, @(SNTRuleTypeBinary), @(SNTRuleStateAllowCompiler)] > 0) {
flushDecisionCache = YES;
break;
// Skip certificate and TeamID rules as they cannot be compiler rules.
if (rule.type == SNTRuleTypeCertificate || rule.type == SNTRuleTypeTeamID) continue;
if ([db longForQuery:@"SELECT COUNT(*) FROM rules WHERE identifier=? AND type IN (?, ?, ?)"
@" AND state=? LIMIT 1",
rule.identifier, @(SNTRuleTypeCDHash), @(SNTRuleTypeBinary),
@(SNTRuleTypeSigningID), @(SNTRuleStateAllowCompiler)] > 0) {
flushDecisionCache = YES;
return;
}
}
}
}];
@@ -538,7 +583,6 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
*error = [NSError errorWithDomain:@"com.google.santad.ruletable" code:code userInfo:d];
return YES;
}
#pragma mark Querying
// Retrieve all rules from the Database

View File

@@ -14,15 +14,19 @@
#import <MOLCertificate/MOLCertificate.h>
#import <MOLCodesignChecker/MOLCodesignChecker.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTRule.h"
#import "Source/common/SNTRuleIdentifiers.h"
#import "Source/santad/DataLayer/SNTRuleTable.h"
/// This test case actually tests SNTRuleTable and SNTRule
@interface SNTRuleTableTest : XCTestCase
@property SNTRuleTable *sut;
@property FMDatabaseQueue *dbq;
@property id mockConfigurator;
@end
@implementation SNTRuleTableTest
@@ -32,6 +36,13 @@
self.dbq = [[FMDatabaseQueue alloc] init];
self.sut = [[SNTRuleTable alloc] initWithDatabaseQueue:self.dbq];
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
}
- (void)tearDown {
[self.mockConfigurator stopMocking];
}
- (SNTRule *)_exampleTeamIDRule {
@@ -56,6 +67,15 @@
return r;
}
- (SNTRule *)_exampleCDHashRule {
SNTRule *r = [[SNTRule alloc] init];
r.identifier = @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a";
r.state = SNTRuleStateBlock;
r.type = SNTRuleTypeCDHash;
r.customMsg = @"A cdhash rule";
return r;
}
- (SNTRule *)_exampleBinaryRule {
SNTRule *r = [[SNTRule alloc] init];
r.identifier = @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670";
@@ -65,6 +85,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 +107,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 +117,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 +167,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,74 +183,72 @@
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
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
signingID:nil
certificateSHA256:nil
teamID:nil];
ruleForIdentifiers:(struct RuleIdentifiers){
.binarySHA256 =
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
}];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
XCTAssertEqual(r.type, SNTRuleTypeBinary);
r = [self.sut
ruleForBinarySHA256:@"b6ee1c3c5a715c049d14a8457faa6b6701b8507efe908300e238e0768bd759c2"
signingID:nil
certificateSHA256:nil
teamID:nil];
ruleForIdentifiers:(struct RuleIdentifiers){
.binarySHA256 =
@"b6ee1c3c5a715c049d14a8457faa6b6701b8507efe908300e238e0768bd759c2",
}];
XCTAssertNil(r);
}
- (void)testFetchCertificateRule {
[self.sut addRules:@[ [self _exampleBinaryRule], [self _exampleCertRule] ]
cleanSlate:NO
ruleCleanup:SNTRuleCleanupNone
error:nil];
SNTRule *r = [self.sut
ruleForBinarySHA256:nil
signingID:nil
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
teamID:nil];
ruleForIdentifiers:(struct RuleIdentifiers){
.certificateSHA256 =
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258",
}];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier,
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
XCTAssertEqual(r.type, SNTRuleTypeCertificate);
r = [self.sut
ruleForBinarySHA256:nil
signingID:nil
certificateSHA256:@"5bdab1288fc16892fef50c658db54f1e2e19cf8f71cc55f77de2b95e051e2562"
teamID:nil];
ruleForIdentifiers:(struct RuleIdentifiers){
.certificateSHA256 =
@"5bdab1288fc16892fef50c658db54f1e2e19cf8f71cc55f77de2b95e051e2562",
}];
XCTAssertNil(r);
}
- (void)testFetchTeamIDRule {
[self.sut addRules:@[ [self _exampleBinaryRule], [self _exampleTeamIDRule] ]
cleanSlate:NO
ruleCleanup:SNTRuleCleanupNone
error:nil];
SNTRule *r = [self.sut ruleForBinarySHA256:nil
signingID:nil
certificateSHA256:nil
teamID:@"ABCDEFGHIJ"];
SNTRule *r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
.teamID = @"ABCDEFGHIJ",
}];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(r.type, SNTRuleTypeTeamID);
XCTAssertEqual([self.sut teamIDRuleCount], 1);
r = [self.sut ruleForBinarySHA256:nil
signingID:nil
certificateSHA256:nil
teamID:@"nonexistentTeamID"];
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
.teamID = @"nonexistentTeamID",
}];
XCTAssertNil(r);
}
@@ -205,84 +257,146 @@
[self _exampleBinaryRule], [self _exampleSigningIDRuleIsPlatform:YES],
[self _exampleSigningIDRuleIsPlatform:NO]
]
cleanSlate:NO
ruleCleanup:SNTRuleCleanupNone
error:nil];
XCTAssertEqual([self.sut signingIDRuleCount], 2);
SNTRule *r = [self.sut ruleForBinarySHA256:nil
signingID:@"ABCDEFGHIJ:signingID"
certificateSHA256:nil
teamID:nil];
SNTRule *r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
.signingID = @"ABCDEFGHIJ:signingID",
}];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ:signingID");
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
r = [self.sut ruleForBinarySHA256:nil
signingID:@"platform:signingID"
certificateSHA256:nil
teamID:nil];
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
.signingID = @"platform:signingID",
}];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"platform:signingID");
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
r = [self.sut ruleForBinarySHA256:nil signingID:@"nonexistent" certificateSHA256:nil teamID:nil];
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
.signingID = @"nonexistent",
}];
XCTAssertNil(r);
}
- (void)testFetchCDHashRule {
[self.sut
addRules:@[ [self _exampleBinaryRule], [self _exampleTeamIDRule], [self _exampleCDHashRule] ]
ruleCleanup:SNTRuleCleanupNone
error:nil];
XCTAssertEqual([self.sut cdhashRuleCount], 1);
SNTRule *r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
.cdhash = @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a",
}];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a");
XCTAssertEqual(r.type, SNTRuleTypeCDHash);
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
.cdhash = @"nonexistent",
}];
XCTAssertNil(r);
}
- (void)testFetchRuleOrdering {
NSError *err;
[self.sut addRules:@[
[self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule],
[self _exampleSigningIDRuleIsPlatform:NO]
[self _exampleCertRule],
[self _exampleBinaryRule],
[self _exampleTeamIDRule],
[self _exampleSigningIDRuleIsPlatform:NO],
[self _exampleCDHashRule],
]
cleanSlate:NO
error:nil];
ruleCleanup:SNTRuleCleanupNone
error:&err];
XCTAssertNil(err);
// This test verifies that the implicit rule ordering we've been abusing is still working.
// See the comment in SNTRuleTable#ruleForBinarySHA256:certificateSHA256:teamID
// See the comment in SNTRuleTable#ruleForIdentifiers:
SNTRule *r = [self.sut
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
signingID:@"ABCDEFGHIJ:signingID"
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
teamID:@"ABCDEFGHIJ"];
ruleForIdentifiers:(struct RuleIdentifiers){
.cdhash = @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a",
.binarySHA256 =
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
.signingID = @"ABCDEFGHIJ:signingID",
.certificateSHA256 =
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258",
.teamID = @"ABCDEFGHIJ",
}];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a");
XCTAssertEqual(r.type, SNTRuleTypeCDHash, @"Implicit rule ordering failed");
r = [self.sut
ruleForIdentifiers:(struct RuleIdentifiers){
.cdhash = @"unknown",
.binarySHA256 =
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
.signingID = @"ABCDEFGHIJ:signingID",
.certificateSHA256 =
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258",
.teamID = @"ABCDEFGHIJ",
}];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
XCTAssertEqual(r.type, SNTRuleTypeBinary, @"Implicit rule ordering failed");
r = [self.sut
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
signingID:@"ABCDEFGHIJ:signingID"
certificateSHA256:@"unknowncert"
teamID:@"ABCDEFGHIJ"];
ruleForIdentifiers:(struct RuleIdentifiers){
.cdhash = @"unknown",
.binarySHA256 =
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
.signingID = @"ABCDEFGHIJ:signingID",
.certificateSHA256 = @"unknown",
.teamID = @"ABCDEFGHIJ",
}];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
XCTAssertEqual(r.type, SNTRuleTypeBinary, @"Implicit rule ordering failed");
r = [self.sut
ruleForBinarySHA256:@"unknown"
signingID:@"unknown"
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
teamID:@"ABCDEFGHIJ"];
ruleForIdentifiers:(struct RuleIdentifiers){
.cdhash = @"unknown",
.binarySHA256 = @"unknown",
.signingID = @"unknown",
.certificateSHA256 =
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258",
.teamID = @"ABCDEFGHIJ",
}];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier,
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
XCTAssertEqual(r.type, SNTRuleTypeCertificate, @"Implicit rule ordering failed");
r = [self.sut ruleForBinarySHA256:@"unknown"
signingID:@"ABCDEFGHIJ:signingID"
certificateSHA256:@"unknown"
teamID:@"ABCDEFGHIJ"];
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
.cdhash = @"unknown",
.binarySHA256 = @"unknown",
.signingID = @"ABCDEFGHIJ:signingID",
.certificateSHA256 = @"unknown",
.teamID = @"ABCDEFGHIJ",
}];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ:signingID");
XCTAssertEqual(r.type, SNTRuleTypeSigningID, @"Implicit rule ordering failed (SigningID)");
r = [self.sut ruleForBinarySHA256:@"unknown"
signingID:@"unknown"
certificateSHA256:@"unknown"
teamID:@"ABCDEFGHIJ"];
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
.cdhash = @"unknown",
.binarySHA256 = @"unknown",
.signingID = @"unknown",
.certificateSHA256 = @"unknown",
.teamID = @"ABCDEFGHIJ",
}];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(r.type, SNTRuleTypeTeamID, @"Implicit rule ordering failed (TeamID)");
@@ -295,7 +409,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];
@@ -308,18 +422,95 @@
- (void)testRetrieveAllRulesWithMultipleRules {
[self.sut addRules:@[
[self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule],
[self _exampleSigningIDRuleIsPlatform:NO]
[self _exampleCertRule],
[self _exampleBinaryRule],
[self _exampleTeamIDRule],
[self _exampleSigningIDRuleIsPlatform:NO],
[self _exampleCDHashRule],
]
cleanSlate:NO
ruleCleanup:SNTRuleCleanupNone
error:nil];
NSArray<SNTRule *> *rules = [self.sut retrieveAllRules];
XCTAssertEqual(rules.count, 4);
XCTAssertEqual(rules.count, 5);
XCTAssertEqualObjects(rules[0], [self _exampleCertRule]);
XCTAssertEqualObjects(rules[1], [self _exampleBinaryRule]);
XCTAssertEqualObjects(rules[2], [self _exampleTeamIDRule]);
XCTAssertEqualObjects(rules[3], [self _exampleSigningIDRuleIsPlatform:NO]);
XCTAssertEqualObjects(rules[4], [self _exampleCDHashRule]);
}
- (void)testAddedRulesShouldFlushDecisionCacheWithNewBlockRule {
// Ensure that a brand new block rule flushes the decision cache.
NSError *error;
SNTRule *r = [self _exampleBinaryRule];
[self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error];
XCTAssertNil(error);
XCTAssertEqual(self.sut.ruleCount, 1);
XCTAssertEqual(self.sut.binaryRuleCount, 1);
// Change the identifer so that the hash of a block rule is not found in the
// db.
r.identifier = @"bfff7d3f6c389ebf7a76a666c484d42ea447834901bc29141439ae7c7b96ff09";
XCTAssertEqual(YES, [self.sut addedRulesShouldFlushDecisionCache:@[ r ]]);
}
// Ensure that a brand new block rule flushes the decision cache.
- (void)testAddedRulesShouldFlushDecisionCacheWithOldBlockRule {
NSError *error;
SNTRule *r = [self _exampleBinaryRule];
[self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error];
XCTAssertNil(error);
XCTAssertEqual(self.sut.ruleCount, 1);
XCTAssertEqual(self.sut.binaryRuleCount, 1);
XCTAssertEqual(NO, [self.sut addedRulesShouldFlushDecisionCache:@[ r ]]);
}
// Ensure that a larger number of blocks flushes the decision cache.
- (void)testAddedRulesShouldFlushDecisionCacheWithLargeNumberOfBlocks {
NSError *error;
SNTRule *r = [self _exampleBinaryRule];
[self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error];
XCTAssertNil(error);
XCTAssertEqual(self.sut.ruleCount, 1);
XCTAssertEqual(self.sut.binaryRuleCount, 1);
NSMutableArray<SNTRule *> *newRules = [NSMutableArray array];
for (int i = 0; i < 1000; i++) {
newRules[i] = r;
}
XCTAssertEqual(YES, [self.sut addedRulesShouldFlushDecisionCache:newRules]);
}
// Ensure that an allow rule that overrides a compiler rule flushes the
// decision cache.
- (void)testAddedRulesShouldFlushDecisionCacheWithCompilerRule {
NSError *error;
SNTRule *r = [self _exampleBinaryRule];
r.type = SNTRuleTypeBinary;
r.state = SNTRuleStateAllowCompiler;
[self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error];
XCTAssertNil(error);
XCTAssertEqual(self.sut.ruleCount, 1);
XCTAssertEqual(self.sut.binaryRuleCount, 1);
// make the rule an allow rule
r.state = SNTRuleStateAllow;
XCTAssertEqual(YES, [self.sut addedRulesShouldFlushDecisionCache:@[ r ]]);
}
// Ensure that an Remove rule targeting an allow rule causes a flush of the cache.
- (void)testAddedRulesShouldFlushDecisionCacheWithRemoveRule {
NSError *error;
SNTRule *r = [self _exampleBinaryRule];
r.type = SNTRuleTypeBinary;
r.state = SNTRuleStateAllow;
[self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error];
XCTAssertNil(error);
XCTAssertEqual(self.sut.ruleCount, 1);
XCTAssertEqual(self.sut.binaryRuleCount, 1);
r.state = SNTRuleStateRemove;
XCTAssertEqual(YES, [self.sut addedRulesShouldFlushDecisionCache:@[ r ]]);
}
@end

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,
@@ -100,13 +152,13 @@ void DARegisterDiskAppearedCallback(DASessionRef session, CFDictionaryRef __null
void DARegisterDiskDisappearedCallback(DASessionRef session, CFDictionaryRef __nullable match,
DADiskDisappearedCallback callback,
void *__nullable context){};
void *__nullable context) {};
void DARegisterDiskDescriptionChangedCallback(DASessionRef session,
CFDictionaryRef __nullable match,
CFArrayRef __nullable watch,
DADiskDescriptionChangedCallback callback,
void *__nullable context){};
void *__nullable context) {};
void DASessionSetDispatchQueue(DASessionRef session, dispatch_queue_t __nullable queue) {
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
@@ -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

@@ -26,6 +26,7 @@
#include <variant>
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/ProcessTree/process_tree.pb.h"
namespace santa::santad::event_providers::endpoint_security {
@@ -71,25 +72,30 @@ class EnrichedProcess {
: effective_user_(std::nullopt),
effective_group_(std::nullopt),
real_user_(std::nullopt),
real_group_(std::nullopt) {}
real_group_(std::nullopt),
annotations_(std::nullopt) {}
EnrichedProcess(std::optional<std::shared_ptr<std::string>> &&effective_user,
std::optional<std::shared_ptr<std::string>> &&effective_group,
std::optional<std::shared_ptr<std::string>> &&real_user,
std::optional<std::shared_ptr<std::string>> &&real_group,
EnrichedFile &&executable)
EnrichedProcess(
std::optional<std::shared_ptr<std::string>> &&effective_user,
std::optional<std::shared_ptr<std::string>> &&effective_group,
std::optional<std::shared_ptr<std::string>> &&real_user,
std::optional<std::shared_ptr<std::string>> &&real_group,
EnrichedFile &&executable,
std::optional<santa::pb::v1::process_tree::Annotations> &&annotations)
: effective_user_(std::move(effective_user)),
effective_group_(std::move(effective_group)),
real_user_(std::move(real_user)),
real_group_(std::move(real_group)),
executable_(std::move(executable)) {}
executable_(std::move(executable)),
annotations_(std::move(annotations)) {}
EnrichedProcess(EnrichedProcess &&other)
: effective_user_(std::move(other.effective_user_)),
effective_group_(std::move(other.effective_group_)),
real_user_(std::move(other.real_user_)),
real_group_(std::move(other.real_group_)),
executable_(std::move(other.executable_)) {}
executable_(std::move(other.executable_)),
annotations_(std::move(other.annotations_)) {}
// Note: Move assignment could be safely implemented but not currently needed
EnrichedProcess &operator=(EnrichedProcess &&other) = delete;
@@ -110,6 +116,10 @@ class EnrichedProcess {
return real_group_;
}
const EnrichedFile &executable() const { return executable_; }
const std::optional<santa::pb::v1::process_tree::Annotations> &annotations()
const {
return annotations_;
}
private:
std::optional<std::shared_ptr<std::string>> effective_user_;
@@ -117,6 +127,7 @@ class EnrichedProcess {
std::optional<std::shared_ptr<std::string>> real_user_;
std::optional<std::shared_ptr<std::string>> real_group_;
EnrichedFile executable_;
std::optional<santa::pb::v1::process_tree::Annotations> annotations_;
};
class EnrichedEventType {
@@ -320,9 +331,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

@@ -18,6 +18,7 @@
#include "Source/common/SantaCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/ProcessTree/process_tree.h"
namespace santa::santad::event_providers::endpoint_security {
@@ -32,7 +33,7 @@ enum class EnrichOptions {
class Enricher {
public:
Enricher();
Enricher(std::shared_ptr<process_tree::ProcessTree> pt = nullptr);
virtual ~Enricher() = default;
virtual std::unique_ptr<EnrichedMessage> Enrich(Message &&msg);
virtual EnrichedProcess Enrich(
@@ -51,6 +52,7 @@ class Enricher {
username_cache_;
SantaCache<gid_t, std::optional<std::shared_ptr<std::string>>>
groupname_cache_;
std::shared_ptr<process_tree::ProcessTree> process_tree_;
};
} // namespace santa::santad::event_providers::endpoint_security

View File

@@ -25,10 +25,14 @@
#include "Source/common/SNTLogging.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/ProcessTree/SNTEndpointSecurityAdapter.h"
#include "Source/santad/ProcessTree/process_tree.h"
#include "Source/santad/ProcessTree/process_tree_macos.h"
namespace santa::santad::event_providers::endpoint_security {
Enricher::Enricher() : username_cache_(256), groupname_cache_(256) {}
Enricher::Enricher(std::shared_ptr<::santa::santad::process_tree::ProcessTree> pt)
: username_cache_(256), groupname_cache_(256), process_tree_(std::move(pt)) {}
std::unique_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
// TODO(mlw): Consider potential design patterns that could help reduce memory usage under load
@@ -74,6 +78,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);
@@ -86,7 +93,10 @@ EnrichedProcess Enricher::Enrich(const es_process_t &es_proc, EnrichOptions opti
UsernameForGID(audit_token_to_egid(es_proc.audit_token), options),
UsernameForUID(audit_token_to_ruid(es_proc.audit_token), options),
UsernameForGID(audit_token_to_rgid(es_proc.audit_token), options),
Enrich(*es_proc.executable, options));
Enrich(*es_proc.executable, options),
process_tree_ ? process_tree_->ExportAnnotations(
process_tree::PidFromAuditToken(es_proc.audit_token))
: std::nullopt);
}
EnrichedFile Enricher::Enrich(const es_file_t &es_file, EnrichOptions options) {

View File

@@ -20,6 +20,8 @@
#include <memory>
#include <string>
#include "Source/santad/ProcessTree/process_tree.h"
namespace santa::santad::event_providers::endpoint_security {
class EndpointSecurityAPI;
@@ -37,15 +39,24 @@ class Message {
Message(const Message& other);
Message& operator=(const Message& other) = delete;
void SetProcessToken(process_tree::ProcessToken tok);
// Operators to access underlying es_message_t
const es_message_t* operator->() const { return es_msg_; }
const es_message_t& operator*() const { return *es_msg_; }
// Helper to get the API associated with this message.
// Used for things like es_exec_arg_count.
// We should ideally rework this to somehow present these functions as methods
// on the Message, however this would be a bit of a bigger lift.
std::shared_ptr<EndpointSecurityAPI> ESAPI() const { return esapi_; }
std::string ParentProcessName() const;
private:
std::shared_ptr<EndpointSecurityAPI> esapi_;
const es_message_t* es_msg_;
std::optional<process_tree::ProcessToken> process_token_;
std::string GetProcessName(pid_t pid) const;
};

View File

@@ -22,7 +22,7 @@
namespace santa::santad::event_providers::endpoint_security {
Message::Message(std::shared_ptr<EndpointSecurityAPI> esapi, const es_message_t *es_msg)
: esapi_(std::move(esapi)), es_msg_(es_msg) {
: esapi_(std::move(esapi)), es_msg_(es_msg), process_token_(std::nullopt) {
esapi_->RetainMessage(es_msg);
}
@@ -36,12 +36,19 @@ Message::Message(Message &&other) {
esapi_ = std::move(other.esapi_);
es_msg_ = other.es_msg_;
other.es_msg_ = nullptr;
process_token_ = std::move(other.process_token_);
other.process_token_ = std::nullopt;
}
Message::Message(const Message &other) {
esapi_ = other.esapi_;
es_msg_ = other.es_msg_;
esapi_->RetainMessage(es_msg_);
process_token_ = other.process_token_;
}
void Message::SetProcessToken(process_tree::ProcessToken tok) {
process_token_ = std::move(tok);
}
std::string Message::ParentProcessName() const {

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

@@ -21,11 +21,13 @@
#include <stdlib.h>
#include <sys/qos.h>
#include <algorithm>
#include <set>
#include <string>
#include <string_view>
#include "Source/common/BranchPrediction.h"
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTLogging.h"
#include "Source/common/SystemResources.h"
@@ -48,7 +50,9 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
"/private/var/db/santa/events.db"};
@interface SNTEndpointSecurityClient ()
@property int64_t deadlineMarginMS;
@property(nonatomic) double defaultBudget;
@property(nonatomic) int64_t minAllowedHeadroom;
@property(nonatomic) int64_t maxAllowedHeadroom;
@property SNTConfigurator *configurator;
@end
@@ -68,10 +72,18 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
if (self) {
_esApi = std::move(esApi);
_metrics = std::move(metrics);
_deadlineMarginMS = 5000;
_configurator = [SNTConfigurator configurator];
_processor = processor;
// Default event processing budget is 80% of the deadline time
_defaultBudget = 0.8;
// For events with small deadlines, clamp processing budget to 1s headroom
_minAllowedHeadroom = 1 * NSEC_PER_SEC;
// For events with large deadlines, clamp processing budget to 5s headroom
_maxAllowedHeadroom = 5 * NSEC_PER_SEC;
_authQueue = dispatch_queue_create(
"com.google.santa.daemon.auth_queue",
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL,
@@ -116,6 +128,10 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
return YES;
}
- (bool)handleContextMessage:(Message &)esMsg {
return false;
}
- (void)establishClientOrDie {
if (self->_esClient.IsConnected()) {
// This is a programming error
@@ -125,7 +141,20 @@ 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 handleContextMessage:esMsg]) {
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
self->_metrics->SetEventMetrics(self->_processor, eventType, EventDisposition::kProcessed,
processingEnd - processingStart);
return;
}
if ([self shouldHandleMessage:esMsg]) {
[self handleMessage:std::move(esMsg)
recordEventMetrics:^(EventDisposition disposition) {
@@ -250,6 +279,24 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
});
}
- (int64_t)computeBudgetForDeadline:(uint64_t)deadline currentTime:(uint64_t)currentTime {
// First get how much time we have left
int64_t nanosUntilDeadline = (int64_t)MachTimeToNanos(deadline - currentTime);
// Compute the desired budget
int64_t budget = nanosUntilDeadline * self.defaultBudget;
// See how much headroom is left
int64_t headroom = nanosUntilDeadline - budget;
// Clamp headroom to maximize budget but ensure it's not so large as to not leave
// enough time to respond in an emergency.
headroom = std::clamp(headroom, self.minAllowedHeadroom, self.maxAllowedHeadroom);
// Return the processing budget given the allotted headroom
return nanosUntilDeadline - headroom;
}
- (void)processMessage:(Message &&)msg handler:(void (^)(const Message &))messageHandler {
if (unlikely(msg->action_type != ES_ACTION_TYPE_AUTH)) {
// This is a programming error
@@ -265,33 +312,33 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
dispatch_semaphore_signal(processingSema);
dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0);
const uint64_t timeout = NSEC_PER_MSEC * (self.deadlineMarginMS);
uint64_t deadlineNano = MachTimeToNanos(msg->deadline - mach_absolute_time());
// TODO(mlw): How should we handle `deadlineNano <= timeout`. Will currently
// result in the deadline block being dispatched immediately (and therefore
// the event will be denied).
int64_t processingBudget = [self computeBudgetForDeadline:msg->deadline
currentTime:mach_absolute_time()];
// Workaround for compiler bug that doesn't properly close over variables
__block Message processMsg = msg;
__block Message deadlineMsg = msg;
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, deadlineNano - timeout), self->_authQueue, ^(void) {
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
// Handler has already responded, nothing to do.
return;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, processingBudget), self->_authQueue, ^(void) {
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
// Handler has already responded, nothing to do.
return;
}
bool res = [self respondToMessage:deadlineMsg
withAuthResult:ES_AUTH_RESULT_DENY
cacheable:false];
es_auth_result_t authResult;
if (self.configurator.failClosed) {
authResult = ES_AUTH_RESULT_DENY;
} else {
authResult = ES_AUTH_RESULT_ALLOW;
}
LOGE(@"SNTEndpointSecurityClient: deadline reached: deny pid=%d, event type: %d ret=%d",
audit_token_to_pid(deadlineMsg->process->audit_token), deadlineMsg->event_type, res);
dispatch_semaphore_signal(deadlineExpiredSema);
});
bool res = [self respondToMessage:deadlineMsg withAuthResult:authResult cacheable:false];
LOGE(@"SNTEndpointSecurityClient: deadline reached: pid=%d, event type: %d, result: %@, ret=%d",
audit_token_to_pid(deadlineMsg->process->audit_token), deadlineMsg->event_type,
(authResult == ES_AUTH_RESULT_DENY ? @"denied" : @"allowed"), res);
dispatch_semaphore_signal(deadlineExpiredSema);
});
dispatch_async(self->_authQueue, ^{
messageHandler(processMsg);

View File

@@ -22,7 +22,9 @@
#include <memory>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SystemResources.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/DataLayer/WatchItemPolicy.h"
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
@@ -48,8 +50,11 @@ using santa::santad::event_providers::endpoint_security::Message;
- (void)handleMessage:(Message &&)esMsg
recordEventMetrics:(void (^)(santa::santad::EventDisposition disposition))recordEventMetrics;
- (BOOL)shouldHandleMessage:(const Message &)esMsg;
- (int64_t)computeBudgetForDeadline:(uint64_t)deadline currentTime:(uint64_t)currentTime;
@property int64_t deadlineMarginMS;
@property(nonatomic) double defaultBudget;
@property(nonatomic) int64_t minAllowedHeadroom;
@property(nonatomic) int64_t maxAllowedHeadroom;
@end
@interface SNTEndpointSecurityClientTest : XCTestCase
@@ -322,11 +327,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 +357,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 = {
@@ -406,11 +417,11 @@ using santa::santad::event_providers::endpoint_security::Message;
metrics:nullptr
processor:Processor::kUnknown];
{
auto enrichedMsg = std::make_unique<EnrichedMessage>(
EnrichedClose(Message(mockESApi, &esMsg),
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
auto enrichedMsg = std::make_unique<EnrichedMessage>(EnrichedClose(
Message(mockESApi, &esMsg),
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
EnrichedFile(std::nullopt, std::nullopt, std::nullopt), std::nullopt),
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
[client processEnrichedMessage:std::move(enrichedMsg)
handler:^(std::unique_ptr<EnrichedMessage> msg) {
@@ -497,7 +508,47 @@ using santa::santad::event_providers::endpoint_security::Message;
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testProcessMessageHandlerWithDeadlineTimeout {
- (void)testComputeBudgetForDeadlineCurrentTime {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
// The test uses crafted values to make even numbers. Ensure the client has
// expected values for these properties so the test can fail early if not.
XCTAssertEqual(client.defaultBudget, 0.8);
XCTAssertEqual(client.minAllowedHeadroom, 1 * NSEC_PER_SEC);
XCTAssertEqual(client.maxAllowedHeadroom, 5 * NSEC_PER_SEC);
std::map<uint64_t, int64_t> deadlineMillisToBudgetMillis{
// Further out deadlines clamp processing budget to maxAllowedHeadroom
{45000, 40000},
// Closer deadlines allow a set percentage processing budget
{15000, 12000},
// Near deadlines clamp processing budget to minAllowedHeadroom
{3500, 2500}};
uint64_t curTime = mach_absolute_time();
for (const auto [deadlineMS, budgetMS] : deadlineMillisToBudgetMillis) {
int64_t got =
[client computeBudgetForDeadline:AddNanosecondsToMachTime(deadlineMS * NSEC_PER_MSEC, curTime)
currentTime:curTime];
// Add 100us, then clip to ms to account for non-exact values due to timebase division
got = (int64_t)((double)(got + (100 * NSEC_PER_USEC)) / (double)NSEC_PER_MSEC);
XCTAssertEqual(got, budgetMS);
}
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)checkDeadlineExpiredFailClosed:(BOOL)shouldFailClosed {
// Set a es_message_t deadline of 750ms
// Set a deadline leeway in the `SNTEndpointSecurityClient` of 500ms
// Mock `RespondFlagsResult` which is called from the deadline handler
@@ -511,7 +562,7 @@ using santa::santad::event_providers::endpoint_security::Message;
// deadlineSema is signaled (or a timeout waiting on deadlineSema)
es_file_t proc_file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&proc_file);
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, &proc, ActionType::Auth,
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth,
750); // 750ms timeout
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
@@ -520,18 +571,27 @@ using santa::santad::event_providers::endpoint_security::Message;
dispatch_semaphore_t deadlineSema = dispatch_semaphore_create(0);
dispatch_semaphore_t controlSema = dispatch_semaphore_create(0);
EXPECT_CALL(*mockESApi, RespondFlagsResult(testing::_, testing::_, 0x0, false))
es_auth_result_t wantAuthResult = shouldFailClosed ? ES_AUTH_RESULT_DENY : ES_AUTH_RESULT_ALLOW;
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, wantAuthResult, false))
.WillOnce(testing::InvokeWithoutArgs(^() {
// Signal deadlineSema to let the handler block continue execution
dispatch_semaphore_signal(deadlineSema);
return true;
}));
id mockConfigurator = OCMClassMock([SNTConfigurator class]);
OCMStub([mockConfigurator configurator]).andReturn(mockConfigurator);
OCMExpect([mockConfigurator failClosed]).andReturn(shouldFailClosed);
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
client.deadlineMarginMS = 500;
// Set min/max headroom the same to clamp the value for this test
client.minAllowedHeadroom = 500 * NSEC_PER_MSEC;
client.maxAllowedHeadroom = 500 * NSEC_PER_MSEC;
{
__block long result;
@@ -560,7 +620,18 @@ using santa::santad::event_providers::endpoint_security::Message;
// seeing the warning (but still possible)
SleepMS(100);
XCTAssertTrue(OCMVerifyAll(mockConfigurator));
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
[mockConfigurator stopMocking];
}
- (void)testDeadlineExpiredFailClosed {
[self checkDeadlineExpiredFailClosed:YES];
}
- (void)testDeadlineExpiredFailOpen {
[self checkDeadlineExpiredFailClosed:NO];
}
@end

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"
@@ -33,6 +34,7 @@
#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h"
#include "Source/santad/Metrics.h"
@@ -49,13 +51,24 @@ class MockAuthResultCache : public AuthResultCache {
MOCK_METHOD(void, FlushCache, (FlushCacheMode mode, FlushCacheReason reason));
};
@interface SNTEndpointSecurityClient (Testing)
@property(nonatomic) double defaultBudget;
@property(nonatomic) int64_t minAllowedHeadroom;
@property(nonatomic) int64_t maxAllowedHeadroom;
@end
@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 +83,9 @@ class MockAuthResultCache : public AuthResultCache {
self.mockDA = [MockDiskArbitration mockDiskArbitration];
[self.mockDA reset];
self.mockMounts = [MockMounts mockMounts];
[self.mockMounts reset];
fclose(stdout);
}
@@ -112,7 +128,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,10 +139,15 @@ 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);
// This test is sensitive to ~1s processing budget.
// Set a 5s headroom and 6s deadline
deviceManager.minAllowedHeadroom = 5 * NSEC_PER_SEC;
deviceManager.maxAllowedHeadroom = 5 * NSEC_PER_SEC;
es_message_t esMsg = MakeESMessage(eventType, &proc, ActionType::Auth, 6000);
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
@@ -211,7 +235,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 +299,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 +329,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 +351,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 +370,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

@@ -17,15 +17,17 @@
#import "Source/santad/EventProviders/AuthResultCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityTreeAwareClient.h"
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
#import "Source/santad/Metrics.h"
#include "Source/santad/ProcessTree/process_tree.h"
#import "Source/santad/SNTCompilerController.h"
/// ES Client focused on subscribing to NOTIFY event variants with the intention of enriching
/// received messages and logging the information.
@interface SNTEndpointSecurityRecorder : SNTEndpointSecurityClient <SNTEndpointSecurityEventHandler>
@interface SNTEndpointSecurityRecorder
: SNTEndpointSecurityTreeAwareClient <SNTEndpointSecurityEventHandler>
- (instancetype)
initWithESAPI:
@@ -38,6 +40,7 @@
compilerController:(SNTCompilerController *)compilerController
authResultCache:
(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache
prefixTree:(std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>>)prefixTree;
prefixTree:(std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>>)prefixTree
processTree:(std::shared_ptr<santa::santad::process_tree::ProcessTree>)processTree;
@end

View File

@@ -13,6 +13,7 @@
/// limitations under the License.
#import "Source/santad/EventProviders/SNTEndpointSecurityRecorder.h"
#include <os/base.h>
#include <EndpointSecurity/EndpointSecurity.h>
@@ -23,6 +24,7 @@
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/Metrics.h"
#include "Source/santad/ProcessTree/process_tree.h"
using santa::common::PrefixTree;
using santa::common::Unit;
@@ -33,10 +35,12 @@ using santa::santad::event_providers::endpoint_security::EnrichedMessage;
using santa::santad::event_providers::endpoint_security::Enricher;
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::Logger;
using santa::santad::process_tree::ProcessTree;
es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
switch (msg->event_type) {
case ES_EVENT_TYPE_NOTIFY_CLOSE: return msg->event.close.target;
case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA: return msg->event.exchangedata.file1;
case ES_EVENT_TYPE_NOTIFY_LINK: return msg->event.link.source;
case ES_EVENT_TYPE_NOTIFY_RENAME: return msg->event.rename.source;
case ES_EVENT_TYPE_NOTIFY_UNLINK: return msg->event.unlink.target;
@@ -62,10 +66,12 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
enricher:(std::shared_ptr<Enricher>)enricher
compilerController:(SNTCompilerController *)compilerController
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache
prefixTree:(std::shared_ptr<PrefixTree<Unit>>)prefixTree {
prefixTree:(std::shared_ptr<PrefixTree<Unit>>)prefixTree
processTree:(std::shared_ptr<ProcessTree>)processTree {
self = [super initWithESAPI:std::move(esApi)
metrics:std::move(metrics)
processor:santa::santad::Processor::kRecorder];
processor:santa::santad::Processor::kRecorder
processTree:std::move(processTree)];
if (self) {
_enricher = enricher;
_logger = logger;
@@ -91,10 +97,10 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
BOOL shouldLogClose = esMsg->event.close.modified;
#if HAVE_MACOS_13
if (@available(macOS 13.5, *)) {
if (esMsg->version >= 6) {
// As of macSO 13.0 we have a new field for if a file was mmaped with
// write permissions on close events. However it did not work until
// 13.5.
// write permissions on close events. However due to a bug in ES, it
// only worked for certain conditions until macOS 13.5 (FB12094635).
//
// If something was mmaped writable it was probably written to. Often
// developer tools do this to avoid lots of write syscalls, e.g. go's
@@ -114,8 +120,28 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
self->_authResultCache->RemoveFromCache(esMsg->event.close.target);
break;
}
default: break;
}
[self.compilerController handleEvent:esMsg withLogger:self->_logger];
switch (esMsg->event_type) {
case ES_EVENT_TYPE_NOTIFY_CLOSE: OS_FALLTHROUGH;
case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA: OS_FALLTHROUGH;
case ES_EVENT_TYPE_NOTIFY_LINK: OS_FALLTHROUGH;
case ES_EVENT_TYPE_NOTIFY_RENAME: OS_FALLTHROUGH;
case ES_EVENT_TYPE_NOTIFY_UNLINK: {
es_file_t *targetFile = GetTargetFileForPrefixTree(&(*esMsg));
if (!targetFile) {
break;
}
// Only log file changes that match the given regex
NSString *targetPath = santa::common::StringToNSString(esMsg->event.close.target->path.data);
NSString *targetPath = santa::common::StringToNSString(targetFile->path.data);
if (![[self.configurator fileChangesRegex]
numberOfMatchesInString:targetPath
options:0
@@ -127,20 +153,26 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
return;
}
if (self->_prefixTree->HasPrefix(targetFile->path.data)) {
recordEventMetrics(EventDisposition::kDropped);
return;
}
break;
}
case ES_EVENT_TYPE_NOTIFY_FORK: OS_FALLTHROUGH;
case ES_EVENT_TYPE_NOTIFY_EXIT: {
if (self.configurator.enableForkAndExitLogging == NO) {
recordEventMetrics(EventDisposition::kDropped);
return;
}
break;
}
default: break;
}
[self.compilerController handleEvent:esMsg withLogger:self->_logger];
// Filter file op events matching the prefix tree.
es_file_t *targetFile = GetTargetFileForPrefixTree(&(*esMsg));
if (targetFile != NULL && self->_prefixTree->HasPrefix(targetFile->path.data)) {
recordEventMetrics(EventDisposition::kDropped);
return;
}
// Enrich the message inline with the ES handler block to capture enrichment
// data as close to the source event as possible.
std::unique_ptr<EnrichedMessage> enrichedMessage = _enricher->Enrich(std::move(esMsg));

View File

@@ -103,7 +103,7 @@ class MockLogger : public Logger {
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
typedef void (^testHelperBlock)(es_message_t *message,
typedef void (^TestHelperBlock)(es_message_t *message,
std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient,
std::shared_ptr<PrefixTree<Unit>> prefixTree,
@@ -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);
}));
@@ -161,7 +157,8 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
enricher:mockEnricher
compilerController:mockCC
authResultCache:mockAuthCache
prefixTree:prefixTree];
prefixTree:prefixTree
processTree:nullptr];
testBlock(&esMsg, mockESApi, mockCC, recorderClient, prefixTree, &sema, &semaMetrics);
@@ -180,7 +177,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
if (@available(macOS 13.0, *)) {
// CLOSE not modified, but was_mapped_writable, should remove from cache,
// and matches fileChangesRegex
testHelperBlock testBlock =
TestHelperBlock 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,
@@ -212,7 +209,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
if (@available(macOS 13.0, *)) {
// CLOSE not modified, but was_mapped_writable, remove from cache, and does not match
// fileChangesRegex
testHelperBlock testBlock =
TestHelperBlock 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,
@@ -236,7 +233,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
- (void)testHandleMessage {
// CLOSE not modified, bail early
testHelperBlock testBlock = ^(
TestHelperBlock 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) {
@@ -250,7 +247,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 +271,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 = ^(
@@ -285,13 +282,52 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
esMsg->event.close.modified = true;
esMsg->event.close.target = &targetFileMissesRegex;
Message msg(mockESApi, esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
recordEventMetrics:^(EventDisposition d) {
XCTFail("Metrics record callback should not be called here");
}]);
};
[self handleMessageWithMatchCalls:NO withMissCalls:YES withBlock:testBlock];
[self handleMessageShouldLog:NO shouldRemoveFromCache:YES withBlock:testBlock];
// UNLINK, remove from cache, but doesn't match fileChangesRegex
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_UNLINK;
esMsg->event.unlink.target = &targetFileMissesRegex;
Message msg(mockESApi, esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
recordEventMetrics:^(EventDisposition d) {
XCTFail("Metrics record callback should not be called here");
}]);
};
[self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock];
// EXCHANGEDATA, Prefix match, bail early
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_UNLINK;
esMsg->event.exchangedata.file1 = &targetFileMatchesRegex;
prefixTree->InsertPrefix(esMsg->event.exchangedata.file1->path.data, Unit{});
Message msg(mockESApi, esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
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];
// LINK, Prefix match, bail early
testBlock =
@@ -316,7 +352,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 {
@@ -325,6 +411,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
extern es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg);
es_file_t closeFile = MakeESFile("close");
es_file_t exchangedataFile = MakeESFile("exchangedata");
es_file_t linkFile = MakeESFile("link");
es_file_t renameFile = MakeESFile("rename");
es_file_t unlinkFile = MakeESFile("unlink");
@@ -347,7 +434,8 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &unlinkFile);
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA;
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
esMsg.event.exchangedata.file1 = &exchangedataFile;
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &exchangedataFile);
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC;
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);

View File

@@ -0,0 +1,30 @@
/// 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.
#include "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/Metrics.h"
#include "Source/santad/ProcessTree/process_tree.h"
@interface SNTEndpointSecurityTreeAwareClient : SNTEndpointSecurityClient
@property std::shared_ptr<santa::santad::process_tree::ProcessTree> processTree;
- (instancetype)
initWithESAPI:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
processor:(santa::santad::Processor)processor
processTree:(std::shared_ptr<santa::santad::process_tree::ProcessTree>)processTree;
@end

View File

@@ -0,0 +1,114 @@
/// 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 "Source/santad/EventProviders/SNTEndpointSecurityTreeAwareClient.h"
#include <EndpointSecurity/EndpointSecurity.h>
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/Metrics.h"
#include "Source/santad/ProcessTree/SNTEndpointSecurityAdapter.h"
#include "Source/santad/ProcessTree/process_tree.h"
#include "Source/santad/ProcessTree/process_tree_macos.h"
using santa::santad::EventDisposition;
using santa::santad::Metrics;
using santa::santad::Processor;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::Message;
@implementation SNTEndpointSecurityTreeAwareClient {
std::vector<bool> _addedEvents;
}
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
metrics:(std::shared_ptr<Metrics>)metrics
processor:(Processor)processor
processTree:
(std::shared_ptr<santa::santad::process_tree::ProcessTree>)processTree {
self = [super initWithESAPI:std::move(esApi) metrics:std::move(metrics) processor:processor];
if (self) {
_processTree = std::move(processTree);
_addedEvents.resize(ES_EVENT_TYPE_LAST, false);
}
return self;
}
// ES guarantees logical consistency within a client (e.g. forks always precede exits),
// however there are no guarantees about the ordering of when messages are delivered _across_
// clients, meaning any client might be the first one to receive process events, and therefore would
// need to be the one to inform the tree. However not all clients are interested in or subscribe to
// process events. This (and the below handleContextMessage) ensures that the ES subscription for
// all clients includes the minimal required set of events for process tree (NOTIFY_FORK, some EXEC
// variant, and NOTIFY_EXIT) but also filters out any events that were subscribed to solely for the
// purpose of updating the tree from being processed downstream, where they would be unexpected.
- (bool)subscribe:(const std::set<es_event_type_t> &)events {
std::set<es_event_type_t> eventsWithLifecycle = events;
if (events.find(ES_EVENT_TYPE_NOTIFY_FORK) == events.end()) {
eventsWithLifecycle.insert(ES_EVENT_TYPE_NOTIFY_FORK);
_addedEvents[ES_EVENT_TYPE_NOTIFY_FORK] = true;
}
if (events.find(ES_EVENT_TYPE_NOTIFY_EXEC) == events.end() &&
events.find(ES_EVENT_TYPE_AUTH_EXEC) == events.end()) {
eventsWithLifecycle.insert(ES_EVENT_TYPE_NOTIFY_EXEC);
_addedEvents[ES_EVENT_TYPE_NOTIFY_EXEC] = true;
}
if (events.find(ES_EVENT_TYPE_NOTIFY_EXIT) == events.end()) {
eventsWithLifecycle.insert(ES_EVENT_TYPE_NOTIFY_EXIT);
_addedEvents[ES_EVENT_TYPE_NOTIFY_EXIT] = true;
}
return [super subscribe:eventsWithLifecycle];
}
- (bool)handleContextMessage:(Message &)esMsg {
if (!_processTree) {
return false;
}
// Inform the tree
switch (esMsg->event_type) {
case ES_EVENT_TYPE_NOTIFY_FORK:
case ES_EVENT_TYPE_NOTIFY_EXEC:
case ES_EVENT_TYPE_AUTH_EXEC:
case ES_EVENT_TYPE_NOTIFY_EXIT:
santa::santad::process_tree::InformFromESEvent(*_processTree, esMsg);
break;
default: break;
}
// Now enumerate the processes that processing this event might require access to...
std::vector<struct santa::santad::process_tree::Pid> pids;
pids.emplace_back(santa::santad::process_tree::PidFromAuditToken(esMsg->process->audit_token));
switch (esMsg->event_type) {
case ES_EVENT_TYPE_AUTH_EXEC:
case ES_EVENT_TYPE_NOTIFY_EXEC:
pids.emplace_back(
santa::santad::process_tree::PidFromAuditToken(esMsg->event.exec.target->audit_token));
break;
case ES_EVENT_TYPE_NOTIFY_FORK:
pids.emplace_back(
santa::santad::process_tree::PidFromAuditToken(esMsg->event.fork.child->audit_token));
break;
default: break;
}
// ...and create the token for those.
esMsg.SetProcessToken(santa::santad::process_tree::ProcessToken(_processTree, std::move(pids)));
return _addedEvents[esMsg->event_type];
}
@end

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"
@@ -143,11 +143,11 @@ class MockWriter : public Null {
mockESApi->SetExpectationsRetainReleaseMessage();
{
auto enrichedMsg = std::make_unique<EnrichedMessage>(
EnrichedClose(Message(mockESApi, &msg),
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
auto enrichedMsg = std::make_unique<EnrichedMessage>(EnrichedClose(
Message(mockESApi, &msg),
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
EnrichedFile(std::nullopt, std::nullopt, std::nullopt), std::nullopt),
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
EXPECT_CALL(*mockSerializer, SerializeMessage(testing::A<const EnrichedClose &>())).Times(1);
EXPECT_CALL(*mockWriter, Write).Times(1);
@@ -229,10 +229,11 @@ class MockWriter : public Null {
EXPECT_CALL(*mockWriter, Write);
Logger(mockSerializer, mockWriter)
.LogFileAccess("v1", "name", Message(mockESApi, &msg),
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
"tgt", FileAccessPolicyDecision::kDenied);
.LogFileAccess(
"v1", "name", Message(mockESApi, &msg),
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
EnrichedFile(std::nullopt, std::nullopt, std::nullopt), std::nullopt),
"tgt", FileAccessPolicyDecision::kDenied);
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());

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;
@@ -93,12 +94,14 @@ std::string GetReasonString(SNTEventState event_state) {
case SNTEventStateAllowScope: return "SCOPE";
case SNTEventStateAllowTeamID: return "TEAMID";
case SNTEventStateAllowSigningID: return "SIGNINGID";
case SNTEventStateAllowCDHash: return "CDHASH";
case SNTEventStateAllowUnknown: return "UNKNOWN";
case SNTEventStateBlockBinary: return "BINARY";
case SNTEventStateBlockCertificate: return "CERT";
case SNTEventStateBlockScope: return "SCOPE";
case SNTEventStateBlockTeamID: return "TEAMID";
case SNTEventStateBlockSigningID: return "SIGNINGID";
case SNTEventStateBlockCDHash: return "CDHASH";
case SNTEventStateBlockLongPath: return "LONG_PATH";
case SNTEventStateBlockUnknown: return "UNKNOWN";
default: return "NOTRUNNING";
@@ -412,6 +415,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"},
@@ -422,6 +436,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
{SNTEventStateBlockScope, "SCOPE"},
{SNTEventStateBlockTeamID, "TEAMID"},
{SNTEventStateBlockSigningID, "SIGNINGID"},
{SNTEventStateBlockCDHash, "CDHASH"},
{SNTEventStateBlockLongPath, "LONG_PATH"},
{SNTEventStateAllowUnknown, "UNKNOWN"},
{SNTEventStateAllowBinary, "BINARY"},
@@ -432,6 +447,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
{SNTEventStateAllowPendingTransitive, "PENDING_TRANSITIVE"},
{SNTEventStateAllowTeamID, "TEAMID"},
{SNTEventStateAllowSigningID, "SIGNINGID"},
{SNTEventStateAllowCDHash, "CDHASH"},
};
for (const auto &kv : stateToReason) {

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);
@@ -181,6 +185,14 @@ static inline void EncodeFileInfoLight(::pbv1::FileInfoLight *pb_file, const es_
pb_file->set_truncated(es_file->path_truncated);
}
static inline void EncodeAnnotations(std::function<::pbv1::process_tree::Annotations *()> lazy_f,
const EnrichedProcess &enriched_proc) {
if (std::optional<pbv1::process_tree::Annotations> proc_annotations = enriched_proc.annotations();
proc_annotations) {
*lazy_f() = *proc_annotations;
}
}
static inline void EncodeProcessInfoLight(::pbv1::ProcessInfoLight *pb_proc_info,
uint32_t message_version, const es_process_t *es_proc,
const EnrichedProcess &enriched_proc) {
@@ -201,6 +213,8 @@ static inline void EncodeProcessInfoLight(::pbv1::ProcessInfoLight *pb_proc_info
enriched_proc.real_group());
EncodeFileInfoLight(pb_proc_info->mutable_executable(), es_proc->executable);
EncodeAnnotations([pb_proc_info] { return pb_proc_info->mutable_annotations(); }, enriched_proc);
}
static inline void EncodeProcessInfo(::pbv1::ProcessInfo *pb_proc_info, uint32_t message_version,
@@ -252,6 +266,8 @@ static inline void EncodeProcessInfo(::pbv1::ProcessInfo *pb_proc_info, uint32_t
if (message_version >= 3) {
EncodeTimestamp(pb_proc_info->mutable_start_time(), es_proc->start_time);
}
EncodeAnnotations([pb_proc_info] { return pb_proc_info->mutable_annotations(); }, enriched_proc);
}
void EncodeExitStatus(::pbv1::Exit *pb_exit, int exitStatus) {
@@ -295,12 +311,14 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
case SNTEventStateAllowScope: return ::pbv1::Execution::REASON_SCOPE;
case SNTEventStateAllowTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
case SNTEventStateAllowSigningID: return ::pbv1::Execution::REASON_SIGNING_ID;
case SNTEventStateAllowCDHash: return ::pbv1::Execution::REASON_CDHASH;
case SNTEventStateAllowUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
case SNTEventStateBlockBinary: return ::pbv1::Execution::REASON_BINARY;
case SNTEventStateBlockCertificate: return ::pbv1::Execution::REASON_CERT;
case SNTEventStateBlockScope: return ::pbv1::Execution::REASON_SCOPE;
case SNTEventStateBlockTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
case SNTEventStateBlockSigningID: return ::pbv1::Execution::REASON_SIGNING_ID;
case SNTEventStateBlockCDHash: return ::pbv1::Execution::REASON_CDHASH;
case SNTEventStateBlockLongPath: return ::pbv1::Execution::REASON_LONG_PATH;
case SNTEventStateBlockUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
default: return ::pbv1::Execution::REASON_NOT_RUNNING;
@@ -448,6 +466,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 +660,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 +738,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);
@@ -416,6 +443,7 @@ void SerializeAndCheckNonESEvents(
{SNTEventStateBlockScope, ::pbv1::Execution::REASON_SCOPE},
{SNTEventStateBlockTeamID, ::pbv1::Execution::REASON_TEAM_ID},
{SNTEventStateBlockSigningID, ::pbv1::Execution::REASON_SIGNING_ID},
{SNTEventStateBlockCDHash, ::pbv1::Execution::REASON_CDHASH},
{SNTEventStateBlockLongPath, ::pbv1::Execution::REASON_LONG_PATH},
{SNTEventStateAllowUnknown, ::pbv1::Execution::REASON_UNKNOWN},
{SNTEventStateAllowBinary, ::pbv1::Execution::REASON_BINARY},
@@ -426,6 +454,7 @@ void SerializeAndCheckNonESEvents(
{SNTEventStateAllowPendingTransitive, ::pbv1::Execution::REASON_PENDING_TRANSITIVE},
{SNTEventStateAllowTeamID, ::pbv1::Execution::REASON_TEAM_ID},
{SNTEventStateAllowSigningID, ::pbv1::Execution::REASON_SIGNING_ID},
{SNTEventStateAllowCDHash, ::pbv1::Execution::REASON_CDHASH},
};
for (const auto &kv : stateToReason) {
@@ -571,6 +600,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 +761,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

@@ -0,0 +1,90 @@
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
load("//:helper.bzl", "santa_unit_test")
cc_library(
name = "process",
hdrs = ["process.h"],
visibility = ["//:santa_package_group"],
deps = [
"//Source/santad/ProcessTree/annotations:annotator",
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/synchronization",
],
)
objc_library(
name = "process_tree",
srcs = [
"process_tree.cc",
"process_tree_macos.mm",
],
hdrs = [
"process_tree.h",
"process_tree_macos.h",
],
sdk_dylibs = [
"bsm",
],
visibility = ["//:santa_package_group"],
deps = [
":process",
"//Source/santad/ProcessTree:process_tree_cc_proto",
"//Source/santad/ProcessTree/annotations:annotator",
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/container:flat_hash_set",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/synchronization",
],
)
proto_library(
name = "process_tree_proto",
srcs = ["process_tree.proto"],
visibility = ["//:santa_package_group"],
)
cc_proto_library(
name = "process_tree_cc_proto",
visibility = ["//:santa_package_group"],
deps = [":process_tree_proto"],
)
objc_library(
name = "SNTEndpointSecurityAdapter",
srcs = ["SNTEndpointSecurityAdapter.mm"],
hdrs = ["SNTEndpointSecurityAdapter.h"],
sdk_dylibs = [
"bsm",
],
visibility = ["//:santa_package_group"],
deps = [
":process_tree",
"//Source/santad:EndpointSecurityAPI",
"//Source/santad:EndpointSecurityMessage",
"@com_google_absl//absl/status:statusor",
],
)
objc_library(
name = "process_tree_test_helpers",
srcs = ["process_tree_test_helpers.mm"],
hdrs = ["process_tree_test_helpers.h"],
deps = [
":process",
":process_tree",
"@com_google_absl//absl/synchronization",
],
)
santa_unit_test(
name = "process_tree_test",
srcs = ["process_tree_test.mm"],
deps = [
":process",
":process_tree_test_helpers",
"//Source/santad/ProcessTree/annotations:annotator",
"@com_google_absl//absl/synchronization",
],
)

View File

@@ -0,0 +1,33 @@
/// 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__SANTAD_PROCESSTREE_SNTENDPOINTSECURITYADAPTER_H
#define SANTA__SANTAD_PROCESSTREE_SNTENDPOINTSECURITYADAPTER_H
#include <EndpointSecurity/EndpointSecurity.h>
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/ProcessTree/process_tree.h"
namespace santa::santad::process_tree {
// Inform the tree of the ES event in msg.
// This is idempotent on the tree, so can be called from multiple places with
// the same msg.
void InformFromESEvent(
ProcessTree &tree,
const santa::santad::event_providers::endpoint_security::Message &msg);
} // namespace santa::santad::process_tree
#endif

View File

@@ -0,0 +1,73 @@
/// 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 "Source/santad/ProcessTree/SNTEndpointSecurityAdapter.h"
#include <EndpointSecurity/EndpointSecurity.h>
#include <Foundation/Foundation.h>
#include <bsm/libbsm.h>
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/ProcessTree/process_tree.h"
#include "Source/santad/ProcessTree/process_tree_macos.h"
#include "absl/status/statusor.h"
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::Message;
namespace santa::santad::process_tree {
void InformFromESEvent(ProcessTree &tree, const Message &msg) {
struct Pid event_pid = PidFromAuditToken(msg->process->audit_token);
auto proc = tree.Get(event_pid);
if (!proc) {
return;
}
std::shared_ptr<EndpointSecurityAPI> esapi = msg.ESAPI();
switch (msg->event_type) {
case ES_EVENT_TYPE_AUTH_EXEC:
case ES_EVENT_TYPE_NOTIFY_EXEC: {
std::vector<std::string> args;
args.reserve(esapi->ExecArgCount(&msg->event.exec));
for (int i = 0; i < esapi->ExecArgCount(&msg->event.exec); i++) {
es_string_token_t arg = esapi->ExecArg(&msg->event.exec, i);
args.push_back(std::string(arg.data, arg.length));
}
es_string_token_t executable = msg->event.exec.target->executable->path;
tree.HandleExec(
msg->mach_time, **proc, PidFromAuditToken(msg->event.exec.target->audit_token),
(struct Program){.executable = std::string(executable.data, executable.length),
.arguments = args},
(struct Cred){
.uid = audit_token_to_euid(msg->event.exec.target->audit_token),
.gid = audit_token_to_egid(msg->event.exec.target->audit_token),
});
break;
}
case ES_EVENT_TYPE_NOTIFY_FORK: {
tree.HandleFork(msg->mach_time, **proc,
PidFromAuditToken(msg->event.fork.child->audit_token));
break;
}
case ES_EVENT_TYPE_NOTIFY_EXIT: tree.HandleExit(msg->mach_time, **proc); break;
default: return;
}
}
} // namespace santa::santad::process_tree

View File

@@ -0,0 +1,11 @@
package(
default_visibility = ["//:santa_package_group"],
)
cc_library(
name = "annotator",
hdrs = ["annotator.h"],
deps = [
"//Source/santad/ProcessTree:process_tree_cc_proto",
],
)

View File

@@ -0,0 +1,40 @@
/// 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__SANTAD_PROCESSTREE_ANNOTATIONS_BASE_H
#define SANTA__SANTAD_PROCESSTREE_ANNOTATIONS_BASE_H
#include <optional>
#include "Source/santad/ProcessTree/process_tree.pb.h"
namespace santa::santad::process_tree {
class ProcessTree;
class Process;
class Annotator {
public:
virtual ~Annotator() = default;
virtual void AnnotateFork(ProcessTree &tree, const Process &parent,
const Process &child) = 0;
virtual void AnnotateExec(ProcessTree &tree, const Process &orig_process,
const Process &new_process) = 0;
virtual std::optional<::santa::pb::v1::process_tree::Annotations> Proto()
const = 0;
};
} // namespace santa::santad::process_tree
#endif

View File

@@ -0,0 +1,114 @@
/// 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__SANTAD_PROCESSTREE_PROCESS_H
#define SANTA__SANTAD_PROCESSTREE_PROCESS_H
#include <sys/types.h>
#include <cstdint>
#include <memory>
#include <string>
#include <typeindex>
#include <vector>
#include "Source/santad/ProcessTree/annotations/annotator.h"
#include "absl/container/flat_hash_map.h"
namespace santa::santad::process_tree {
struct Pid {
pid_t pid;
uint64_t pidversion;
friend bool operator==(const struct Pid &lhs, const struct Pid &rhs) {
return lhs.pid == rhs.pid && lhs.pidversion == rhs.pidversion;
}
friend bool operator!=(const struct Pid &lhs, const struct Pid &rhs) {
return !(lhs == rhs);
}
};
template <typename H>
H AbslHashValue(H h, const struct Pid &p) {
return H::combine(std::move(h), p.pid, p.pidversion);
}
struct Cred {
uid_t uid;
gid_t gid;
friend bool operator==(const struct Cred &lhs, const struct Cred &rhs) {
return lhs.uid == rhs.uid && lhs.gid == rhs.gid;
}
friend bool operator!=(const struct Cred &lhs, const struct Cred &rhs) {
return !(lhs == rhs);
}
};
struct Program {
std::string executable;
std::vector<std::string> arguments;
friend bool operator==(const struct Program &lhs, const struct Program &rhs) {
return lhs.executable == rhs.executable && lhs.arguments == rhs.arguments;
}
friend bool operator!=(const struct Program &lhs, const struct Program &rhs) {
return !(lhs == rhs);
}
};
// Fwd decls
class ProcessTree;
class Process {
public:
explicit Process(const Pid pid, const Cred cred,
std::shared_ptr<const Program> program,
std::shared_ptr<const Process> parent)
: pid_(pid),
effective_cred_(cred),
program_(program),
annotations_(),
parent_(parent),
refcnt_(0),
tombstoned_(false) {}
Process(const Process &) = default;
Process &operator=(const Process &) = delete;
Process(Process &&) = default;
Process &operator=(Process &&) = delete;
// Const "attributes" are public
const struct Pid pid_;
const struct Cred effective_cred_;
const std::shared_ptr<const Program> program_;
private:
// This is not API.
// The tree helper methods are the API, and we just happen to implement
// annotation storage and the parent relation in memory on the process right
// now.
friend class ProcessTree;
absl::flat_hash_map<std::type_index, std::shared_ptr<const Annotator>>
annotations_;
std::shared_ptr<const Process> parent_;
// TODO(nickmg): atomic here breaks the build.
int refcnt_;
// If the process is tombstoned, the event removing it from the tree has been
// processed, but refcnt>0 keeps it alive.
bool tombstoned_;
};
} // namespace santa::santad::process_tree
#endif

View File

@@ -0,0 +1,316 @@
/// 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 "Source/santad/ProcessTree/process_tree.h"
#include <sys/types.h>
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <typeindex>
#include <utility>
#include <vector>
#include "Source/santad/ProcessTree/annotations/annotator.h"
#include "Source/santad/ProcessTree/process.h"
#include "Source/santad/ProcessTree/process_tree.pb.h"
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/synchronization/mutex.h"
namespace santa::santad::process_tree {
void ProcessTree::BackfillInsertChildren(
absl::flat_hash_map<pid_t, std::vector<Process>> &parent_map,
std::shared_ptr<Process> parent, const Process &unlinked_proc) {
auto proc = std::make_shared<Process>(
unlinked_proc.pid_, unlinked_proc.effective_cred_,
// Re-use shared pointers from parent if value equivalent
(parent && *(unlinked_proc.program_) == *(parent->program_))
? parent->program_
: unlinked_proc.program_,
parent);
{
absl::MutexLock lock(&mtx_);
map_.emplace(unlinked_proc.pid_, proc);
}
// The only case where we should not have a parent is the root processes
// (e.g. init, kthreadd).
if (parent) {
for (auto &annotator : annotators_) {
annotator->AnnotateFork(*this, *(proc->parent_), *proc);
if (proc->program_ != proc->parent_->program_) {
annotator->AnnotateExec(*this, *(proc->parent_), *proc);
}
}
}
for (const Process &child : parent_map[unlinked_proc.pid_.pid]) {
BackfillInsertChildren(parent_map, proc, child);
}
}
void ProcessTree::HandleFork(uint64_t timestamp, const Process &parent,
const Pid new_pid) {
if (Step(timestamp)) {
std::shared_ptr<Process> child;
{
absl::MutexLock lock(&mtx_);
child = std::make_shared<Process>(new_pid, parent.effective_cred_,
parent.program_, map_[parent.pid_]);
map_.emplace(new_pid, child);
}
for (const auto &annotator : annotators_) {
annotator->AnnotateFork(*this, parent, *child);
}
}
}
void ProcessTree::HandleExec(uint64_t timestamp, const Process &p,
const Pid new_pid, const Program prog,
const Cred c) {
if (Step(timestamp)) {
// TODO(nickmg): should struct pid be reworked and only pid_version be
// passed?
assert(new_pid.pid == p.pid_.pid);
auto new_proc = std::make_shared<Process>(
new_pid, c, std::make_shared<const Program>(prog), p.parent_);
{
absl::MutexLock lock(&mtx_);
remove_at_.push_back({timestamp, p.pid_});
map_.emplace(new_proc->pid_, new_proc);
}
for (const auto &annotator : annotators_) {
annotator->AnnotateExec(*this, p, *new_proc);
}
}
}
void ProcessTree::HandleExit(uint64_t timestamp, const Process &p) {
if (Step(timestamp)) {
absl::MutexLock lock(&mtx_);
remove_at_.push_back({timestamp, p.pid_});
}
}
bool ProcessTree::Step(uint64_t timestamp) {
absl::MutexLock lock(&mtx_);
uint64_t new_cutoff = seen_timestamps_.front();
if (timestamp < new_cutoff) {
// Event timestamp is before the rolling list of seen events.
// This event may or may not have been processed, but be conservative and
// do not reprocess.
return false;
}
// seen_timestamps_ is sorted, so only look for the value if it's possibly
// within the array.
if (timestamp < seen_timestamps_.back()) {
// TODO(nickmg): If array is made bigger, replace with a binary search.
for (const auto seen_ts : seen_timestamps_) {
if (seen_ts == timestamp) {
// Event seen, signal it should not be reprocessed.
return false;
}
}
}
auto insert_point =
std::find_if(seen_timestamps_.rbegin(), seen_timestamps_.rend(),
[&](uint64_t x) { return x < timestamp; });
std::move(seen_timestamps_.begin() + 1, insert_point.base(),
seen_timestamps_.begin());
*insert_point = timestamp;
for (auto it = remove_at_.begin(); it != remove_at_.end();) {
if (it->first < new_cutoff) {
if (auto target = GetLocked(it->second);
target && (*target)->refcnt_ > 0) {
(*target)->tombstoned_ = true;
} else {
map_.erase(it->second);
}
it = remove_at_.erase(it);
} else {
it++;
}
}
return true;
}
void ProcessTree::RetainProcess(std::vector<struct Pid> &pids) {
absl::MutexLock lock(&mtx_);
for (const struct Pid &p : pids) {
auto proc = GetLocked(p);
if (proc) {
(*proc)->refcnt_++;
}
}
}
void ProcessTree::ReleaseProcess(std::vector<struct Pid> &pids) {
absl::MutexLock lock(&mtx_);
for (const struct Pid &p : pids) {
auto proc = GetLocked(p);
if (proc) {
if (--(*proc)->refcnt_ == 0 && (*proc)->tombstoned_) {
map_.erase(p);
}
}
}
}
/*
---
Annotation get/set
---
*/
void ProcessTree::AnnotateProcess(const Process &p,
std::shared_ptr<const Annotator> a) {
absl::MutexLock lock(&mtx_);
const Annotator &x = *a;
map_[p.pid_]->annotations_.emplace(std::type_index(typeid(x)), std::move(a));
}
std::optional<::santa::pb::v1::process_tree::Annotations>
ProcessTree::ExportAnnotations(const Pid p) {
auto proc = Get(p);
if (!proc || (*proc)->annotations_.empty()) {
return std::nullopt;
}
::santa::pb::v1::process_tree::Annotations a;
for (const auto &[_, annotation] : (*proc)->annotations_) {
if (auto x = annotation->Proto(); x) a.MergeFrom(*x);
}
return a;
}
/*
---
Tree inspection methods
---
*/
std::vector<std::shared_ptr<const Process>> ProcessTree::RootSlice(
std::shared_ptr<const Process> p) const {
std::vector<std::shared_ptr<const Process>> slice;
while (p) {
slice.push_back(p);
p = p->parent_;
}
return slice;
}
void ProcessTree::Iterate(
std::function<void(std::shared_ptr<const Process> p)> f) const {
std::vector<std::shared_ptr<const Process>> procs;
{
absl::ReaderMutexLock lock(&mtx_);
procs.reserve(map_.size());
for (auto &[_, proc] : map_) {
procs.push_back(proc);
}
}
for (auto &p : procs) {
f(p);
}
}
std::optional<std::shared_ptr<const Process>> ProcessTree::Get(
const Pid target) const {
absl::ReaderMutexLock lock(&mtx_);
return GetLocked(target);
}
std::optional<std::shared_ptr<Process>> ProcessTree::GetLocked(
const Pid target) const {
auto it = map_.find(target);
if (it == map_.end()) {
return std::nullopt;
}
return it->second;
}
std::shared_ptr<const Process> ProcessTree::GetParent(const Process &p) const {
return p.parent_;
}
#if SANTA_PROCESS_TREE_DEBUG
void ProcessTree::DebugDump(std::ostream &stream) const {
absl::ReaderMutexLock lock(&mtx_);
stream << map_.size() << " processes" << std::endl;
DebugDumpLocked(stream, 0, 0);
}
void ProcessTree::DebugDumpLocked(std::ostream &stream, int depth,
pid_t ppid) const
ABSL_SHARED_LOCKS_REQUIRED(mtx_) {
for (auto &[_, process] : map_) {
if ((ppid == 0 && !process->parent_) ||
(process->parent_ && process->parent_->pid_.pid == ppid)) {
stream << std::string(2 * depth, ' ') << process->pid_.pid
<< process->program_->executable << std::endl;
DebugDumpLocked(stream, depth + 1, process->pid_.pid);
}
}
}
#endif
absl::StatusOr<std::shared_ptr<ProcessTree>> CreateTree(
std::vector<std::unique_ptr<Annotator>> annotations) {
absl::flat_hash_set<std::type_index> seen;
for (const auto &annotator : annotations) {
if (seen.count(std::type_index(typeid(annotator)))) {
return absl::InvalidArgumentError(
"Multiple annotators of the same class");
}
seen.emplace(std::type_index(typeid(annotator)));
}
if (seen.empty()) {
return nullptr;
}
auto tree = std::make_shared<ProcessTree>(std::move(annotations));
if (auto status = tree->Backfill(); !status.ok()) {
return status;
}
return tree;
}
/*
----
Tokens
----
*/
ProcessToken::ProcessToken(std::shared_ptr<ProcessTree> tree,
std::vector<struct Pid> pids)
: tree_(std::move(tree)), pids_(std::move(pids)) {
tree_->RetainProcess(pids);
}
ProcessToken::~ProcessToken() { tree_->ReleaseProcess(pids_); }
} // namespace santa::santad::process_tree

View File

@@ -0,0 +1,189 @@
/// 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__SANTAD_PROCESSTREE_TREE_H
#define SANTA__SANTAD_PROCESSTREE_TREE_H
#include <memory>
#include <typeinfo>
#include <vector>
#include "Source/santad/ProcessTree/process.h"
#include "absl/container/flat_hash_map.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/synchronization/mutex.h"
namespace santa::santad::process_tree {
absl::StatusOr<Process> LoadPID(pid_t pid);
// Fwd decl for test peer.
class ProcessTreeTestPeer;
class ProcessTree {
public:
explicit ProcessTree(std::vector<std::unique_ptr<Annotator>> &&annotators)
: annotators_(std::move(annotators)), seen_timestamps_({}) {}
ProcessTree(const ProcessTree &) = delete;
ProcessTree &operator=(const ProcessTree &) = delete;
ProcessTree(ProcessTree &&) = delete;
ProcessTree &operator=(ProcessTree &&) = delete;
// Initialize the tree with the processes currently running on the system.
absl::Status Backfill();
// Inform the tree of a fork event, in which the parent process spawns a child
// with the only difference between the two being the pid.
void HandleFork(uint64_t timestamp, const Process &parent,
struct Pid new_pid);
// Inform the tree of an exec event, in which the program and potentially cred
// of a Process change.
// p is the process performing the exec (running the "old" program),
// and new_pid, prog, and cred are the new pid, program, and credentials
// after the exec.
// N.B. new_pid is required as the "pid version" will have changed.
// It is a programming error to pass a new_pid such that
// p.pid_.pid != new_pid.pid.
void HandleExec(uint64_t timestamp, const Process &p, struct Pid new_pid,
struct Program prog, struct Cred c);
// Inform the tree of a process exit.
void HandleExit(uint64_t timestamp, const Process &p);
// Mark the given pids as needing to be retained in the tree's map for future
// access. Normally, Processes are removed once all clients process past the
// event which would remove the Process (e.g. exit), however in cases where
// async processing occurs, the Process may need to be accessed after the
// exit.
void RetainProcess(std::vector<struct Pid> &pids);
// Release previously retained processes, signaling that the client is done
// processing the event that retained them.
void ReleaseProcess(std::vector<struct Pid> &pids);
// Annotate the given process with an Annotator (state).
void AnnotateProcess(const Process &p, std::shared_ptr<const Annotator> a);
// Get the given annotation on the given process if it exists, or nullopt if
// the annotation is not set.
template <typename T>
std::optional<std::shared_ptr<const T>> GetAnnotation(const Process &p) const;
// Get the fully merged proto form of all annotations on the given process.
std::optional<::santa::pb::v1::process_tree::Annotations> ExportAnnotations(
struct Pid p);
// Atomically get the slice of Processes going from the given process "up"
// to the root. The root process has no parent. N.B. There may be more than
// one root process. E.g. on Linux, both init (PID 1) and kthread (PID 2)
// are considered roots, as they are reported to have PPID=0.
std::vector<std::shared_ptr<const Process>> RootSlice(
std::shared_ptr<const Process> p) const;
// Call f for all processes in the tree. The list of processes is captured
// before invoking f, so it is safe to mutate the tree in f.
void Iterate(std::function<void(std::shared_ptr<const Process>)> f) const;
// Get the Process for the given pid in the tree if it exists.
std::optional<std::shared_ptr<const Process>> Get(struct Pid target) const;
// Traverse the tree from the given Process to its parent.
std::shared_ptr<const Process> GetParent(const Process &p) const;
#if SANTA_PROCESS_TREE_DEBUG
// Dump the tree in a human readable form to the given ostream.
void DebugDump(std::ostream &stream) const;
#endif
private:
friend class ProcessTreeTestPeer;
void BackfillInsertChildren(
absl::flat_hash_map<pid_t, std::vector<Process>> &parent_map,
std::shared_ptr<Process> parent, const Process &unlinked_proc);
// Mark that an event with the given timestamp is being processed.
// Returns whether the given timestamp is "novel", and the tree should be
// updated with the results of the event.
bool Step(uint64_t timestamp);
std::optional<std::shared_ptr<Process>> GetLocked(struct Pid target) const
ABSL_SHARED_LOCKS_REQUIRED(mtx_);
void DebugDumpLocked(std::ostream &stream, int depth, pid_t ppid) const;
std::vector<std::unique_ptr<Annotator>> annotators_;
mutable absl::Mutex mtx_;
absl::flat_hash_map<const struct Pid, std::shared_ptr<Process>> map_
ABSL_GUARDED_BY(mtx_);
// List of pids which should be removed from map_, and at the timestamp at
// which they should be.
// Elements are removed when the timestamp falls out of the seen_timestamps_
// list below, signifying that all clients have synced past the timestamp.
std::vector<std::pair<uint64_t, struct Pid>> remove_at_ ABSL_GUARDED_BY(mtx_);
// Rolling list of event timestamps processed by the tree.
// This is used to ensure an event only gets processed once, even if events
// come out of order.
std::array<uint64_t, 32> seen_timestamps_ ABSL_GUARDED_BY(mtx_);
};
template <typename T>
std::optional<std::shared_ptr<const T>> ProcessTree::GetAnnotation(
const Process &p) const {
auto it = p.annotations_.find(std::type_index(typeid(T)));
if (it == p.annotations_.end()) {
return std::nullopt;
}
return std::dynamic_pointer_cast<const T>(it->second);
}
// Create a new tree, ensuring the provided annotations are valid and that
// backfill is successful.
absl::StatusOr<std::shared_ptr<ProcessTree>> CreateTree(
std::vector<std::unique_ptr<Annotator>> annotations);
// ProcessTokens provide a lifetime based approach to retaining processes
// in a ProcessTree. When a token is created with a list of pids that may need
// to be referenced during processing of a given event, the ProcessToken informs
// the tree to retain those pids in its map so any call to ProcessTree::Get()
// during event processing succeeds. When the token is destroyed, it signals the
// tree to release the pids, which removes them from the tree if they would have
// fallen out otherwise due to a destruction event (e.g. exit).
class ProcessToken {
public:
explicit ProcessToken(std::shared_ptr<ProcessTree> tree,
std::vector<struct Pid> pids);
~ProcessToken();
ProcessToken(const ProcessToken &other)
: ProcessToken(other.tree_, other.pids_) {}
ProcessToken(ProcessToken &&other) noexcept
: tree_(std::move(other.tree_)), pids_(std::move(other.pids_)) {}
ProcessToken &operator=(const ProcessToken &other) {
return *this = ProcessToken(other.tree_, other.pids_);
}
ProcessToken &operator=(ProcessToken &&other) noexcept {
tree_ = std::move(other.tree_);
pids_ = std::move(other.pids_);
return *this;
}
private:
std::shared_ptr<ProcessTree> tree_;
std::vector<struct Pid> pids_;
};
} // namespace santa::santad::process_tree
#endif

View File

@@ -0,0 +1,6 @@
syntax = "proto3";
package santa.pb.v1.process_tree;
message Annotations {
}

View File

@@ -0,0 +1,26 @@
/// 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__SANTAD_PROCESSTREE_TREE_MACOS_H
#define SANTA__SANTAD_PROCESSTREE_TREE_MACOS_H
#include <bsm/libbsm.h>
namespace santa::santad::process_tree {
// Create a struct pid from the given audit token.
struct Pid PidFromAuditToken(const audit_token_t &tok);
} // namespace santa::santad::process_tree
#endif

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