Compare commits

...

108 Commits

Author SHA1 Message Date
Matt W
be87b3eaf2 Change types of repeated args and envs fields (#1063)
* Change types of repeated args and envs fields

* Update args and env testdata strings to base64

* Remove whitespace
2023-03-31 13:18:09 -04:00
Russell Hancox
0fe672817e sync: Fix case of empty header name (#1062) 2023-03-28 11:50:11 -04:00
Russell Hancox
c3b2fbf512 sync: Allow server to override the header for transmitting XSRF tokens (#1060)
This change allows a sync server to change the header that Santa will use to send XSRF tokens on subsequent requests by putting the header name in the  header.
2023-03-27 18:11:11 -04:00
Matt W
2984d98cb9 Document SigningID and PlatformBinary exception keys (#1059)
* Document SigningID and PlatformBinary exception keys

* Minor spacing
2023-03-25 11:34:06 -04:00
Nick Gregory
5295faef0e Fix a couple last TSAN failures (#1056)
* Skip testHandleMessage when testing with tsan

* fix other 2 tsan failures

* change action_env->test_env in bazelrc for sanitizers

* revert Source/santactl/BUILD formatting
2023-03-23 11:11:29 -04:00
Liam Nicholson
0209344f62 santad: Fix SD Card Block not operating on Internal SD Card Readers (#1055) 2023-03-22 17:54:11 -04:00
Matt W
53ca5eb811 Support filesystem monitoring config embedded in main Santa config (#1054)
* Allow setting file access policy in main Santa config

* Add some tests
2023-03-20 16:47:34 -04:00
Matt W
33c7aab9f1 Basic rate limiting for File Access Authorizer (#1053)
* WIP basic rate limiting support

* WIP added basic metrics when rate limited

* Hookup new metrics

* Cleanup old TODO

* PR feedback, update comments
2023-03-20 15:58:49 -04:00
Pål-Magnus Slåtto
f6d837ac31 chore(ci): Upgrade workflows to non-deprecated runtimes (#1052) 2023-03-15 09:42:16 -04:00
Matt W
5e0a383662 Properly report "file access client enabled" metrics (#1051) 2023-03-14 15:01:03 -04:00
Russell Hancox
8055b451bb Config: Ignore static rules with an invalid identifier (#1049) 2023-03-07 10:33:13 -05:00
Russell Hancox
c5e7736eef santactl/rule: Validate identifier is a valid SHA-256 for binary/cert rules (#1045)
Previously validation only applied when using the --sha256 flag, now it applies to --identifier too unless adding a team ID rule. The validation is also a bit more robust.

Fixes #1044
2023-03-01 13:44:44 -05:00
Matt W
61558048c0 Add basic metrics to report when the FAM client is enabled (#1043) 2023-02-17 11:57:18 -05:00
Matt W
cf0e3fd3db Add support for platform binary to process exceptions (#1041)
* Add support for platform bianry to process exceptions

* Fun with bool types
2023-02-17 11:30:46 -05:00
Matt W
15519c6de8 Clear ES cache when watch items change (#1042) 2023-02-17 11:04:08 -05:00
Pete Markowsky
a415679980 Fix sync protocol diagram. (#1037) 2023-02-08 16:13:08 -05:00
Nick Gregory
27ae60e265 Small test fixes to make sanitizers happy (#1030)
* Small test fixes to make sanitizers happy

* lint

* missing authclient

* new MockEndpointSecurityAPI per subtest
2023-02-06 20:16:22 +00:00
Matt W
29a50f072c Report log type in santactl status (#1036)
* Report log type in santactl status

* Remove unnecessary fallback case
2023-02-06 14:59:42 -05:00
Matt W
a97e82e316 Replace SNTDecisionCache dictionary with SantaCache (#1034)
* Replace SNTDecisionCache dictionary with SantaCache

* PR feedback. Fix tests.
2023-02-03 15:58:53 -05:00
Russell Hancox
532120ac02 Configurator: Return an unsafe_unretained pointer to avoid needless retain/release (#1035) 2023-02-03 15:55:15 -05:00
Russell Hancox
ec934854fc santactl & syncservice: Use synchronousRemoteObjectProxy where it makes sense (#1033) 2023-02-03 14:31:37 -05:00
Matt W
ad0e2abdac Restart daemon on log type change (#1031)
* WIP register for event log type changes. Flush metrics.

* Add Flush to writer interface. Flush logger on log type change.

* Standardize non-thread-safe method names
2023-02-03 11:04:57 -05:00
Matt W
dc11ea6534 Rework timeout handling in metrics HTTP writer (#1029)
* Change HTTP writer to use session config timeouts

* Remove unnecessary block variable

* Fix tests

* Revert serializer changes for now

* Remove setting timeoutIntervalForRequest
2023-02-02 10:58:28 -05:00
Matt W
3acf3c1d00 Use cached sizes when serializing (#1028) 2023-01-30 16:08:38 -05:00
Matt W
41bc3d2542 Perf: Translocate cache, reserve proto repeated fields (#1027)
* Translocate cache, reserve proto repeated fields

* Remove copy/paste
2023-01-30 12:18:32 -05:00
Pete Markowsky
45a5d4e800 Fix: Rewrite the SNTMetricHTTPWriter to avoid potential stack corruption (#1019)
* Updated the SNTMetricHTTPWriter to use a for loop to prevent crashes caused by writing to stop.

* Make requests serial again.

* Fix the typo,  I just pushed.

* Ensure we only lookup the timeout value once.

* Make SNTConfigurator assignment only happen once.
2023-01-30 11:53:26 -05:00
Matt W
82bd981f31 Fix team ID and signing ID checks (#1026)
* Fix policy checks with missing team/signing ids

* Update docs to clarify how symbolic links are handled
2023-01-30 09:14:27 -05:00
Russell Hancox
6480d9c99b docs: fix width of sidebar on larger windows (#1025) 2023-01-27 15:38:46 -05:00
Henry S
7e963080b3 add updated description (#1023)
Zentral has gained many more Santa-specific workflows since adding to this section in 2017. The updated description takes this into account.
2023-01-27 15:38:14 -05:00
Matt W
e58cd7d125 Remove Default column (#1024) 2023-01-27 15:28:31 -05:00
Russell Hancox
db597e413b docs: Support wider pages, fix syntax highlighting of plist (#1022) 2023-01-27 15:18:45 -05:00
Matt W
78f46896d5 Try with more vertical space (#1021) 2023-01-27 14:37:02 -05:00
Matt W
cc0742dbfb Fsmon docs table width (#1020)
* markdown spaces lol

* markdown vertical spaces lol

* more spaces why not
2023-01-27 14:32:58 -05:00
Matt W
9c2f76af72 Initial docs for file access auth feature (#1017)
* Initial docs for file access auth feature

* Apply suggestions from code review

Co-authored-by: Kathryn Hancox <44557882+kathancox@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Kathryn Hancox <44557882+kathancox@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Kathryn Hancox <44557882+kathancox@users.noreply.github.com>

* Updates based on PR feedback

---------

Co-authored-by: Kathryn Hancox <44557882+kathancox@users.noreply.github.com>
2023-01-27 14:08:34 -05:00
Matt W
a3ed5ccb40 Log type metrics (#1018)
* Add event log type to metrics

* lint

* PR Feedback
2023-01-27 10:22:09 -05:00
Nick Gregory
b4149816c7 Add new continuous test run with various sanitizers (#1016)
* continuous tests with sanitizer matrix

* dyld insert lib

* remove msan config and upload logs
2023-01-26 16:00:47 -05:00
Matt W
2313d6338d Remove extra expectation in test (#1015) 2023-01-26 11:42:14 -05:00
Russell Hancox
414fbff721 Project: Fix module maps for swift libraries and their dependencies (#1014) 2023-01-26 09:15:30 -05:00
Matt W
5a2e42e9b4 Reduce calls into configurator (#1013) 2023-01-25 16:51:13 -05:00
Matt W
f8d1b2e880 Reduce proto warning severity (#1012) 2023-01-25 14:37:00 -05:00
Matt W
5f4d2a92fc Ensure watch item names conform to naming requirements (#1011)
* Ensure watch item names conform to naming requirements

* Only compile regex once
2023-01-25 13:27:27 -05:00
Russell Hancox
4ccffdca01 GUI: Migrate DeviceMessageWindow to SwiftUI (#1010) 2023-01-25 12:16:31 -05:00
Nick Gregory
e60bbe1b55 shadow rules_python for fuzzing (#1009) 2023-01-23 11:11:48 -05:00
Russell Hancox
eee2149439 GUI: Re-write AboutWindow view in SwiftUI (#1007) 2023-01-20 13:43:50 -05:00
Russell Hancox
dcbbc33e5e Revert "Configurator: Apply config updates in non-daemon processes (#1003)" (#1008)
This reverts commit 1e88b88ee6.
2023-01-20 13:30:06 -05:00
Matt W
ebe5166d77 Prevent recursive reconnect attempts (#1005) 2023-01-19 10:03:15 -05:00
Matt W
6e5a530df5 Low hanging fruit perf changes (#1004)
* Some minor changes for some easy perf wins based on trace info

* Manually track buffer offsets in File writer

* Add metrics tests

* Call members from appropriate shared object
2023-01-18 15:14:48 -05:00
Russell Hancox
1e88b88ee6 Configurator: Apply config updates in non-daemon processes (#1003) 2023-01-18 10:00:39 -05:00
Nick Gregory
2d74f36ddb Reconnect to santametrics service on failure (#1001)
* Reconnect to santametrics service on failure

* use logging macros
2023-01-12 10:41:36 -05:00
Matt W
3a3564f36b Add watch item state to santactl status (#1000)
* Add method to get WatchItems state

* Update santactl status with watch items state

* Update status label

* PR feedback - add missing dispatch_group_leave
2023-01-12 10:38:12 -05:00
Matt W
d3c7cbbcc3 Rename type aliases (#999) 2023-01-11 11:30:11 -05:00
Matt W
1ff6967934 Support configuring signing IDs for process exceptions (#998) 2023-01-11 09:42:32 -05:00
Matt W
53877f6114 Adopt new FS Access Auth config format and policy application logic (#994)
* WIP parsing new watch item config format

* Change WatchItemPolicy param order. Define policy default constants.

* rename write_only policy member to allow_read_access

* WIP parsing new config format, WatchItemsTest all pass

* Restructured process config parsing. Added tons of tests.

* Abstract NSError creation to a function

* Better errors. Bubble up NSErrors to reduce duplicate messages. More Tests.

* Validate min string lengths. Add a bunch more tests.

* Adopt new policy process logic and add tests

* Address PR feedback
2023-01-10 16:40:13 -05:00
Matt W
8c50af4041 Add policy version and name to basic string serializer (#997) 2023-01-10 13:17:21 -05:00
Russell Hancox
d0d4508f77 docs: Fix deployment/configuration doc (#996) 2023-01-10 09:23:52 -05:00
Matt W
df3aac5baf Change name of santa config keys for file access monitoring (#995) 2023-01-09 21:08:57 -05:00
Nick Gregory
e289056e5e lower fuzz case timeout to 5s (#993) 2023-01-09 12:28:45 -05:00
Matt W
4adad2ecfa More event type support (#992)
* Add truncate and create support

* Add metrics support
2023-01-06 12:51:40 -05:00
Matt W
dc1a3c27c2 Add more event coverage in the file access client (#991)
* Support more file access protection event types

* Update tests for new events and method signatures

* lint

* Add metrics for new event types

* Add support for LINK event

* Fix spacing
2023-01-05 13:03:21 -05:00
Nick Gregory
a2f8030482 Fuzz embedded plist reading (#990)
* fuzz embedded plist reading

* remove newline

* consolidate size checking

* brackets

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>
2023-01-03 14:22:31 -05:00
Matt W
338a4f738f Opportunistically use ES cache when possible (#989)
* WIP fixing up ES cacheability in file access client

* Removed old code from before simplification

* Add more tests
2023-01-03 14:09:21 -05:00
Nick Gregory
845d72eebd Fix nightly run cron specification (#986) 2022-12-28 17:36:54 -05:00
Nick Gregory
ca81270bff Fix SNTFileInfo Fuzzing (#985)
* fix SNTFileInfo fd leak

* auto poweroff VM after fuzzing

* lint
2022-12-27 16:20:32 -05:00
Matt W
42cf1b232a Adopt new ES APIs to watch target paths in tamper client (#984) 2022-12-22 16:49:25 -05:00
Nick Gregory
57285c48dd use new public api for booting VM into recoveryOS (#983) 2022-12-22 16:27:38 -05:00
Nick Gregory
2279cd8662 Run fuzzing in a VM (#982)
* run fuzzing in a vm

* no use cleaning up since each VM is pristine
2022-12-22 13:52:46 -05:00
Nick Gregory
9423beecc8 fix spinloop when no override config is specified (#981) 2022-12-22 13:51:46 -05:00
Pete Markowsky
b18d4a0e30 Fix SNTFileInfoTest for macOS 13 (#977)
* Fix SNTFileInfoTest for macOS 13
2022-12-22 10:37:22 -05:00
Russell Hancox
290ebed15e Allstar: Add fuzzing artifact (#980) 2022-12-22 09:13:53 -05:00
Matt W
435868aa7a Add build targets, lint (#978) 2022-12-21 22:56:20 -05:00
Nick Gregory
2e3952a31d Revitalize Fuzzing (#976)
* snapshot using rules_fuzzing, but this probably wont work because nothing supports objc

* working fuzz

* clean up

* install libclang_rt.fuzzer_osx automatically; add to CI

* retain corpus

* restore old fuzzing stuff

* corpus

* move fuzz to separate timed action

* review
2022-12-21 15:29:07 -05:00
Matt W
60f53bc20a Adopt new ES APIs to monitor target paths (#975)
* WIP begin adopting new ES APIs inverting target mute paths

* Track subscription status so as not to unnecessarily enable/disable

* Properly chain call to invert target mute paths. Fix using wrong Message obj.

* Add base client tests

* Support compiling on older platforms

* More changes to support compiling on older platforms

* Only enable watch items periodic task on macOS 13

* Add more asserts to test

* Disable ES caching for now

* lint
2022-12-20 21:15:01 -05:00
Russell Hancox
fec3766da4 Project: Upgrade MOLAuthenticatingURLSession to v3.1 (#974) 2022-12-19 15:12:09 -05:00
Matt W
ae63055f34 Fix golden test data for macOS 13 (#972) 2022-12-19 14:47:06 -05:00
Russell Hancox
e5a0c3c1c0 sync: Fix deduplication in reachability handler (#973) 2022-12-19 14:42:18 -05:00
Matt W
5680c69164 Address policy consistency issues (#971)
* Change FindPolicyForPath to operate on vector of inputs

* Adopt new interface to find all policies simultaneously

* Fix tests to use new FindPoliciesForPath signature
2022-12-19 14:20:05 -05:00
Nick Gregory
8a978c1e75 Update LICENSE for VM code (#970) 2022-12-16 12:21:49 -05:00
Matt W
6aa7c9ba86 Fix import issues (#969)
* Fix import issues

* lint
2022-12-15 16:10:26 -05:00
Matt W
6adef6a714 Track path types for current/new watch items (#968)
* Move WatchItemPolicy to its own header. Add path type enum.

* When tracking current/new paths, also track path types

* lint
2022-12-15 15:44:47 -05:00
Nick Gregory
1d8c105257 absl_guarded_by (#967) 2022-12-15 13:34:32 -05:00
Matt W
e2d7cf04fc Fix under retain (#966) 2022-12-15 12:50:59 -05:00
Nick Gregory
9d448071f7 Lint the E2E start-vm Python script (#965)
* appease the linter

* add python to the lint script
2022-12-14 17:37:56 -05:00
Nick Gregory
cd6c0e7120 Introduce end-to-end testing (#919)
* initial e2e work

* switch to entitlements property instead of codesignopts hack

* bring moroz patches in

* go ahead and switch to upstream

* lint

* no need to install gcs every time

* codeowners

* add comments

* move to new e2e workflow

* rename e2e workflow
2022-12-14 11:15:55 -05:00
Matt W
ec5e8177fb Serialize File Access events (#964)
* WIP skeleton code for file access event serialization

* Added basic string serializer for file access event

* Added proto string serializer for file access event
2022-12-14 11:04:37 -05:00
Russell Hancox
8e10c103cb santad: Flush cache when StaticRules are changed (#963) 2022-12-13 16:57:13 -05:00
Matt W
db6c14ea10 Enrich file access events, prepare for logging (#962)
* WIP refactor file access class to setup logging

* Combined GetPathTarget1 and 2, added some tests.

* Change method name to not be abbrv.

* Remove unnecessary includes

* PR feedback: fix missing path sep, add comments

* Fix test issue
2022-12-12 16:37:47 -05:00
Matt W
4a4f1a971c Fix issue where wrong variable was used (#961) 2022-12-09 15:07:30 -05:00
Matt W
c5c82a18ff Dynamically enable/disable FS Access client based on config (#959)
* WIP Dynamic watch item config loading. Dynamic event handler protocol.

* Clients can now register with WatchItems to be enabled/disabled

* Handle dynamic fs monitor config add/modify/delete, dynamic enable/disable clients

* Update WatchItemsTest to use new constructor

* Better check handling value changes

* Add missing mock config value to fix integration test
2022-12-09 11:54:54 -05:00
Russell Hancox
f702c7a281 Tests: Fix SNTEndpointSecurityFileAccessAuthorizerTest (#958) 2022-12-08 15:46:51 -05:00
Russell Hancox
958ef52698 Config: In debug builds, allow config to be overriden from a plist file. (#957) 2022-12-08 15:07:59 -05:00
Matt W
068ec885b2 pemdas (#955)
* pemdas

* lint
2022-12-07 17:07:05 -05:00
Matt W
e572f047c0 Import fix (#953) 2022-12-07 14:07:13 -05:00
Matt W
b904a329d9 FS Access Config Version, Policy decision enums (#951)
* Add policy version to config. Return policy decision as enum.

* Check EnableBadSignatureProtection config when evaluating instigating procs

* Draft proto update for file access

* Revert "Draft proto update for file access"

This reverts commit 5d7e9a9e03.

* Change return type to work around OCMock partial mocking issues

* lint
2022-12-07 13:33:35 -05:00
Matt W
d19343bccd Draft proto for new FileAccess log (#952)
* Draft proto for new FileAccess log

* Update Source/common/santa.proto

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

Co-authored-by: Pete Markowsky <pmarkowsky@users.noreply.github.com>
2022-12-07 13:06:47 -05:00
Matt W
09cd78d756 Initial work for File Access Authorizer Client (#949)
* WIP Initial work for new fs watcher client

* WIP basic working mechanics of applying policy to OPEN events

* WIP now support allowing access based on cdhash

* WIP lint fix

* WIP check instigator cdhash and cert hash against policy

* WIP Fix test issue in base ES client class

* WIP Fix test issue in water items test

* Added secondary lookup cache for cert hashes and fallback lookups

* Adopt new SantaVnode name

* Adopt min macOS 11. Adopt new SantaCacheHasher for SantaVnode.

* Rename the es client to FileAccessAuthorizer

* Added some more tests

* Added MockLogger and a lot more tests.

* Removed currently unused subscriptions. Don't enable FS client by default

* lint

* lint after rebase

* Use strtoul for hex string conversion. Update comments.

* PR feedback
2022-12-06 19:52:32 -05:00
Russell Hancox
f169b69944 santad: Change workaround for glob header with blocks, free glob-allocated memory (#948) 2022-12-05 15:52:17 -05:00
Russell Hancox
40f9872c54 Tests: Fix some assertions comparing strings (#947) 2022-12-05 12:54:04 -05:00
Matt W
5718f2e582 Watch items (#937)
* WIP started work on parsing config

* WIP Basics of parsing config and generating new policy

* WIP Reapplying config updates functionally complete. Needs a lot more tests.

* Test cleanup, added using decl for watch items tree type

* More WatchItems tests and test polishing.

* Remove test print function. Formatting.

* Commented use of __BLOCKS__ undef

* Return a shared_ptr from factory

* Change WatchItemsPolicy to store sets instead of vectors

* Remove unnecessary WatchItem, replace with string

* Typo

* Update error messages to not make it sound like parse errors are recoverable
2022-12-01 13:41:05 -05:00
Liam Nicholson
04fd742114 Include SD Card Mounting in the USB Block Functionality (#938) 2022-12-01 10:25:54 -05:00
Matt W
194a3a6d4a Remove SNTCommon (#945)
* Move santa_action_t to SNTCommonEnums and rename to SNTAction

* Move likely and unlikely macros to a new BranchPrediction header

* Remove SNTCommon.h. Move SantaVnode to its own header.

* Add SantaVnodeHash

* Fix build deps
2022-12-01 09:14:54 -05:00
Matt W
e1dc50fb36 Drop macOS 10.15 (#944)
* Drop macOS 10.15 support

* lint
2022-11-29 20:20:48 -05:00
Matt W
9ff2f0d631 Swtich from task_info to libproc for system resource info (#939)
* Swtich from task_info to libproc for system resource info

* Fix return value

* Convert nanos to seconds

* Make GetTimebase static. Expose NanosToMachTime.

* Abstract return or GetTaskInfo to new type.
2022-11-29 16:50:37 -05:00
Matt W
85058ec290 Rename santa_vnode_id_t to SantaVnode (#943)
* Rename santa_vnode_id_t to SantaVnode. Add factory.

* Change types of SantaVnode to match stat(2)
2022-11-28 23:45:14 -05:00
Russell Hancox
6e90673f71 docs: Update keyserver address in SECURITY (#941) 2022-11-28 19:35:02 -05:00
Russell Hancox
a58cee908f docs: Fix typo in sync-protocol, h/t to @maxwbuckley (#940) 2022-11-28 17:21:45 -05:00
Russell Hancox
80b26955b4 GUI: Fix distributed notifications in silent mode (#936) 2022-11-16 09:53:56 -05:00
Matt W
6a84023548 Prefix tree updates (#931)
* WIP Rename SNTPrefixTree to PrefixTree

* WIP Implement the new PrefixTree and tests

* Add Unit type. Fix build and tests.

* lint

* Make NodeCount accessor for tests

* Updated comments
2022-11-14 13:16:49 +00:00
Russell Hancox
e70acefb5c Docs: Fix type of {allowed,blocked}_path_regex keys in preflight (#934) 2022-11-07 15:36:10 -05:00
231 changed files with 12125 additions and 2193 deletions

View File

@@ -1,5 +1,6 @@
# Ignore reason: These crafted binaries are used in tests
ignorePaths:
- Fuzzing/common/MachOParse_corpus/ret0
- Source/common/testdata/bad_pagezero
- Source/common/testdata/missing_pagezero
- Source/common/testdata/missing_pagezero

View File

@@ -9,11 +9,34 @@ build --cxxopt=-std=c++17
build --copt=-DSANTA_OPEN_SOURCE=1
build --cxxopt=-DSANTA_OPEN_SOURCE=1
build:asan --strip=never
build:asan --copt="-Wno-macro-redefined"
build:asan --copt="-D_FORTIFY_SOURCE=0"
build:asan --copt="-O1"
build:asan --copt="-fno-omit-frame-pointer"
# Many config options for sanitizers pulled from
# https://github.com/protocolbuffers/protobuf/blob/main/.bazelrc
build:san-common --strip=never
build:san-common --copt="-Wno-macro-redefined"
build:san-common --copt="-D_FORTIFY_SOURCE=0"
build:san-common --copt="-O1"
build:san-common --copt="-fno-omit-frame-pointer"
build:asan --config=san-common
build:asan --copt="-fsanitize=address"
build:asan --copt="-DADDRESS_SANITIZER"
build:asan --linkopt="-fsanitize=address"
build:asan --test_env="ASAN_OPTIONS=log_path=/tmp/san_out"
build:tsan --config=san-common
build:tsan --copt="-fsanitize=thread"
build:tsan --copt="-DTHREAD_SANITIZER=1"
build:tsan --linkopt="-fsanitize=thread"
build:tsan --test_env="TSAN_OPTIONS=log_path=/tmp/san_out:halt_on_error=true"
build:ubsan --config=san-common
build:ubsan --copt="-fsanitize=undefined"
build:ubsan --copt="-DUNDEFINED_SANITIZER=1"
build:ubsan --copt="-fno-sanitize=function" --copt="-fno-sanitize=vptr"
build:ubsan --linkopt="-fsanitize=undefined"
build:ubsan --test_env="UBSAN_OPTIONS=log_path=/tmp/san_out"
build:fuzz --config=san-common
build:fuzz --@rules_fuzzing//fuzzing:cc_engine=@rules_fuzzing//fuzzing/engines:libfuzzer
build:fuzz --@rules_fuzzing//fuzzing:cc_engine_instrumentation=libfuzzer
build:fuzz --@rules_fuzzing//fuzzing:cc_engine_sanitizer=asan

View File

@@ -16,7 +16,7 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Run linters
run: ./Testing/lint.sh
@@ -24,28 +24,28 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-10.15, macos-11, macos-12]
os: [macos-11, macos-12]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Build Userspace
run: bazel build --apple_generate_dsym -c opt :release --define=SANTA_BUILD_TYPE=adhoc
- uses: actions/checkout@v3
- name: Build Userspace
run: bazel build --apple_generate_dsym -c opt :release --define=SANTA_BUILD_TYPE=adhoc
unit_tests:
strategy:
fail-fast: false
matrix:
os: [macos-10.15, macos-11, macos-12]
os: [macos-11, macos-12]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Run All Tests
run: bazel test :unit_tests --define=SANTA_BUILD_TYPE=adhoc --test_output=errors
test_coverage:
runs-on: macos-11
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Generate test coverage
run: sh ./generate_cov.sh
- name: Coveralls

View File

@@ -1,13 +1,13 @@
name: continuous
on:
schedule:
- cron: '* 10 * * *' # Every day at 10:00 UTC
- cron: '0 10 * * *' # Every day at 10:00 UTC
workflow_dispatch: # Allows you to run this workflow manually from the Actions tab
jobs:
preqs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Checks for flaky tests
run: bazel test --test_strategy=exclusive --test_output=errors --runs_per_test 50 -t- :unit_tests --define=SANTA_BUILD_TYPE=adhoc

41
.github/workflows/e2e.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: E2E
on: workflow_dispatch
jobs:
start_vm:
runs-on: e2e-host
steps:
- uses: actions/checkout@v3
- name: Start VM
run: python3 Testing/integration/actions/start_vm.py macOS_12.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
- name: Add homebrew to PATH
run: echo "/opt/homebrew/bin/" >> $GITHUB_PATH
- 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
- name: Test sync server changes
run: ./Testing/integration/test_sync_changes.sh
- name: Poweroff
if: ${{ always() }}
run: sudo shutdown -h +1

35
.github/workflows/fuzz.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Fuzzing
on:
schedule:
- cron: '0 6 * * *' # Every day at 6:00 UTC
workflow_dispatch: # Allows you to run this workflow manually from the Actions tab
jobs:
start_vm:
runs-on: e2e-host
steps:
- uses: actions/checkout@v3
- name: Start VM
run: python3 Testing/integration/actions/start_vm.py macOS_13.bundle.tar.gz
fuzz:
runs-on: e2e-vm
steps:
- uses: actions/checkout@v3
- name: Setup libfuzzer
run: Fuzzing/install_libclang_fuzzer.sh
- name: Fuzz
run: |
for target in $(bazel query 'kind(fuzzing_launcher, //Fuzzing:all)'); do
bazel run --config=fuzz $target -- -- -max_len=32768 -runs=1000000 -timeout=5
done
- name: Upload crashes
uses: actions/upload-artifact@v1
if: failure()
with:
name: artifacts
path: /tmp/fuzzing/artifacts
- name: Poweroff VM
if: ${{ always() }}
run: sudo shutdown -h +1

30
.github/workflows/sanitizers.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: sanitizers
on:
schedule:
- cron: '0 16 * * *'
workflow_dispatch:
jobs:
test:
runs-on: macos-latest
strategy:
matrix:
sanitizer: [asan, tsan, ubsan]
steps:
- uses: actions/checkout@v3
- name: ${{ matrix.sanitizer }}
run: |
CLANG_VERSION=$(clang --version | head -n 1 | cut -d' ' -f 4)
DYLIB_PATH="$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/${CLANG_VERSION}/lib/darwin/libclang_rt.${{ matrix.sanitizer }}_osx_dynamic.dylib"
bazel test --config=${{ matrix.sanitizer }} \
--test_strategy=exclusive --test_output=errors \
--test_env=DYLD_INSERT_LIBRARIES=${DYLIB_PATH} \
--runs_per_test 5 -t- :unit_tests \
--define=SANTA_BUILD_TYPE=adhoc
- name: Upload logs
uses: actions/upload-artifact@v1
if: failure()
with:
name: logs
path: /tmp/san_out*

429
.pylintrc Normal file
View File

@@ -0,0 +1,429 @@
# This Pylint rcfile contains a best-effort configuration to uphold the
# best-practices and style described in the Google Python style guide:
# https://google.github.io/styleguide/pyguide.html
#
# Its canonical open-source location is:
# https://google.github.io/styleguide/pylintrc
[MASTER]
# Files or directories to be skipped. They should be base names, not paths.
ignore=third_party
# Files or directories matching the regex patterns are skipped. The regex
# matches against base names, not paths.
ignore-patterns=
# Pickle collected data for later comparisons.
persistent=no
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Use multiple processes to speed up Pylint.
jobs=4
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=abstract-method,
apply-builtin,
arguments-differ,
attribute-defined-outside-init,
backtick,
bad-option-value,
basestring-builtin,
buffer-builtin,
c-extension-no-member,
consider-using-enumerate,
cmp-builtin,
cmp-method,
coerce-builtin,
coerce-method,
delslice-method,
div-method,
duplicate-code,
eq-without-hash,
execfile-builtin,
file-builtin,
filter-builtin-not-iterating,
fixme,
getslice-method,
global-statement,
hex-method,
idiv-method,
implicit-str-concat,
import-error,
import-self,
import-star-module-level,
inconsistent-return-statements,
input-builtin,
intern-builtin,
invalid-str-codec,
locally-disabled,
long-builtin,
long-suffix,
map-builtin-not-iterating,
misplaced-comparison-constant,
missing-function-docstring,
metaclass-assignment,
next-method-called,
next-method-defined,
no-absolute-import,
no-else-break,
no-else-continue,
no-else-raise,
no-else-return,
no-init, # added
no-member,
no-name-in-module,
no-self-use,
nonzero-method,
oct-method,
old-division,
old-ne-operator,
old-octal-literal,
old-raise-syntax,
parameter-unpacking,
print-statement,
raising-string,
range-builtin-not-iterating,
raw_input-builtin,
rdiv-method,
reduce-builtin,
relative-import,
reload-builtin,
round-builtin,
setslice-method,
signature-differs,
standarderror-builtin,
suppressed-message,
sys-max-int,
too-few-public-methods,
too-many-ancestors,
too-many-arguments,
too-many-boolean-expressions,
too-many-branches,
too-many-instance-attributes,
too-many-locals,
too-many-nested-blocks,
too-many-public-methods,
too-many-return-statements,
too-many-statements,
trailing-newlines,
unichr-builtin,
unicode-builtin,
unnecessary-pass,
unpacking-in-except,
useless-else-on-loop,
useless-object-inheritance,
useless-suppression,
using-cmp-argument,
wrong-import-order,
xrange-builtin,
zip-builtin-not-iterating,
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages
reports=no
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
[BASIC]
# Good variable names which should always be accepted, separated by a comma
good-names=main,_
# Bad variable names which should always be refused, separated by a comma
bad-names=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl
# Regular expression matching correct function names
function-rgx=^(?:(?P<exempt>setUp|tearDown|setUpModule|tearDownModule)|(?P<camel_case>_?[A-Z][a-zA-Z0-9]*)|(?P<snake_case>_?[a-z][a-z0-9_]*))$
# Regular expression matching correct variable names
variable-rgx=^[a-z][a-z0-9_]*$
# Regular expression matching correct constant names
const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
# Regular expression matching correct attribute names
attr-rgx=^_{0,2}[a-z][a-z0-9_]*$
# Regular expression matching correct argument names
argument-rgx=^[a-z][a-z0-9_]*$
# Regular expression matching correct class attribute names
class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
# Regular expression matching correct inline iteration names
inlinevar-rgx=^[a-z][a-z0-9_]*$
# Regular expression matching correct class names
class-rgx=^_?[A-Z][a-zA-Z0-9]*$
# Regular expression matching correct module names
module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$
# Regular expression matching correct method names
method-rgx=(?x)^(?:(?P<exempt>_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P<camel_case>_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P<snake_case>_{0,2}[a-z][a-z0-9_]*))$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=10
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=80
# TODO(https://github.com/PyCQA/pylint/issues/3352): Direct pylint to exempt
# lines made too long by directives to pytype.
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=(?x)(
^\s*(\#\ )?<?https?://\S+>?$|
^\s*(from\s+\S+\s+)?import\s+.+$)
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=yes
# Maximum number of lines in a module
max-module-lines=99999
# String used as indentation unit. The internal Google style guide mandates 2
# spaces. Google's externaly-published style guide says 4, consistent with
# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google
# projects (like TensorFlow).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=TODO
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=yes
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_)
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging,absl.logging,tensorflow.io.logging
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,
TERMIOS,
Bastion,
rexec,
sets
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant, absl
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls,
class_
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=StandardError,
Exception,
BaseException

1
CODEOWNERS Normal file
View File

@@ -0,0 +1 @@
* @google/macendpoints

11
Fuzzing/BUILD Normal file
View File

@@ -0,0 +1,11 @@
load("fuzzing.bzl", "objc_fuzz_test")
objc_fuzz_test(
name = "MachOParse",
srcs = ["common/MachOParse.mm"],
corpus = glob(["common/MachOParse_corpus/*"]),
linkopts = ["-lsqlite3"],
deps = [
"//Source/common:SNTFileInfo",
],
)

View File

@@ -0,0 +1,40 @@
#import <Foundation/Foundation.h>
#include <libproc.h>
#include <stddef.h>
#include <stdint.h>
#import "Source/common/SNTFileInfo.h"
int get_num_fds() {
return proc_pidinfo(getpid(), PROC_PIDLISTFDS, 0, NULL, 0) / PROC_PIDLISTFD_SIZE;
}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
static NSString *tmpPath =
[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]];
int num_fds_pre = get_num_fds();
@autoreleasepool {
NSData *input = [NSData dataWithBytesNoCopy:(void *)data length:size freeWhenDone:false];
[input writeToFile:tmpPath atomically:false];
NSError *error;
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithResolvedPath:tmpPath error:&error];
if (!fi || error != nil) {
NSLog(@"Error: %@", error);
return -1;
}
// Mach-O Parsing
[fi architectures];
[fi isMissingPageZero];
[fi infoPlist];
}
if (num_fds_pre != get_num_fds()) {
abort();
}
return 0;
}

Binary file not shown.

20
Fuzzing/fuzzing.bzl Normal file
View File

@@ -0,0 +1,20 @@
"""Utilities for fuzzing Santa"""
load("@rules_fuzzing//fuzzing:cc_defs.bzl", "cc_fuzz_test")
def objc_fuzz_test(name, srcs, deps, corpus, linkopts = [], **kwargs):
native.objc_library(
name = "%s_lib" % name,
srcs = srcs,
deps = deps,
**kwargs
)
cc_fuzz_test(
name = name,
deps = [
"%s_lib" % name,
],
linkopts = linkopts,
corpus = corpus,
)

View File

@@ -0,0 +1,14 @@
#!/bin/bash
# Xcode doesn't include the fuzzer runtime, but the one LLVM ships is compatible with Apple clang.
set -uexo pipefail
CLANG_VERSION=$(clang --version | head -n 1 | cut -d' ' -f 4)
DST_PATH="$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/${CLANG_VERSION}/lib/darwin/libclang_rt.fuzzer_osx.a"
if [ -f ${DST_PATH} ]; then
exit 0;
fi
curl -O -L https://github.com/llvm/llvm-project/releases/download/llvmorg-${CLANG_VERSION}/clang+llvm-${CLANG_VERSION}-x86_64-apple-darwin.tar.xz
tar xvf clang+llvm-${CLANG_VERSION}-x86_64-apple-darwin.tar.xz clang+llvm-${CLANG_VERSION}-x86_64-apple-darwin/lib/clang/${CLANG_VERSION}/lib/darwin/libclang_rt.fuzzer_osx.a
cp clang+llvm-${CLANG_VERSION}-x86_64-apple-darwin/lib/clang/${CLANG_VERSION}/lib/darwin/libclang_rt.fuzzer_osx.a ${DST_PATH}

View File

@@ -1,4 +0,0 @@
bin
llvm-*.src
llvm-*.src.tar.xz

View File

@@ -1,109 +0,0 @@
#!/usr/bin/env bash
LLVM_VERSION='5.0.1'
LLVM_COMPILERRT_TARBALL_NAME="llvm-${LLVM_VERSION}.src.tar.xz"
LLVM_COMPILERRT_SRC_FOLDER_NAME=`echo "${LLVM_COMPILERRT_TARBALL_NAME}" | cut -d '.' -f 1-4`
LLVM_COMPILERRT_TARBALL_URL="http://releases.llvm.org/${LLVM_VERSION}/${LLVM_COMPILERRT_TARBALL_NAME}"
LIBFUZZER_FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
LOG_FILE=`mktemp`
main() {
echo "libFuzzer build script"
echo " > Checking dependencies..."
checkDependencies || return 1
echo " > Entering libFuzzer folder..."
cd "${LIBFUZZER_FOLDER}" > /dev/null 2>&1
if [ $? -ne 0 ] ; then
echo "Failed to enter the libFuzzer folder: ${LIBFUZZER_FOLDER}"
return 1
fi
if [ ! -f "${LLVM_COMPILERRT_TARBALL_NAME}" ] ; then
echo " > Downloading the LLVM tarball..."
curl "${LLVM_COMPILERRT_TARBALL_URL}" -o "${LLVM_COMPILERRT_TARBALL_NAME}" > "${LOG_FILE}" 2>&1
if [ $? -ne 0 ] ; then
dumpLogFile "Failed to download the LLVM tarball"
return 1
fi
else
echo " > An existing LLVM tarball was found"
fi
if [ -d "${LLVM_COMPILERRT_SRC_FOLDER_NAME}" ] ; then
echo " > Deleting existing LLVM folder..."
rm -rf "${LLVM_COMPILERRT_SRC_FOLDER_NAME}" > "${LOG_FILE}" 2>&1
if [ $? -ne 0 ] ; then
dumpLogFile "Failed to delete the existing source folder"
return 1
fi
fi
echo " > Extracting the LLVM tarball..."
tar xf "${LLVM_COMPILERRT_TARBALL_NAME}" > "${LOG_FILE}" 2>&1
if [ $? -ne 0 ] ; then
rm "${LLVM_COMPILERRT_TARBALL_NAME}" "${LLVM_COMPILERRT_SRC_FOLDER_NAME}"
dumpLogFile "Failed to extract the LLVM tarball"
return 1
fi
if [ -d "bin" ] ; then
echo " > Deleting existing bin folder..."
rm -rf "bin" > "${LOG_FILE}" 2>&1
if [ $? -ne 0 ] ; then
dumpLogFile "Failed to delete the existing bin folder"
return 1
fi
fi
mkdir "bin" > "${LOG_FILE}" 2>&1
if [ $? -ne 0 ] ; then
dumpLogFile "Failed to create the bin folder"
return 1
fi
echo " > Building libFuzzer..."
( cd "bin" && "../${LLVM_COMPILERRT_SRC_FOLDER_NAME}/lib/Fuzzer/build.sh" ) > "${LOG_FILE}" 2>&1
if [ $? -ne 0 ] ; then
dumpLogFile "Failed to build the library"
return 1
fi
printf "\nFinished building libFuzzer\n"
rm "${LOG_FILE}"
return 0
}
checkDependencies() {
executable_list=( "clang++" "curl" "tar" )
for executable in "${executable_list[@]}" ; do
which "${executable}" > /dev/null 2>&1
if [ $? -ne 0 ] ; then
echo "The following program was not found: ${executable}"
return 1
fi
done
return 0
}
dumpLogFile() {
if [ $# -eq 1 ] ; then
local message="$1"
else
local message="An error has occurred"
fi
printf "${message}\n"
printf "Log file follows\n===\n"
cat "${LOG_FILE}"
printf "\n===\n"
rm "${LOG_FILE}"
}
main $@
exit $?

View File

@@ -20,6 +20,7 @@
#import "SNTCommandController.h"
#import "SNTRule.h"
#import "SNTXPCControlInterface.h"
#import "Source/common/SNTCommonEnums.h"
extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, std::size_t size) {
if (size > 16) {
@@ -28,7 +29,7 @@ extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, std::size_t size
return 1;
}
santa_vnode_id_t vnodeID = {};
SantaVnode vnodeID = {};
std::memcpy(&vnodeID, data, size);
MOLXPCConnection *daemonConn = [SNTXPCControlInterface configuredConnection];
@@ -41,14 +42,14 @@ extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, std::size_t size
[[daemonConn remoteObjectProxy]
checkCacheForVnodeID:vnodeID
withReply:^(santa_action_t action) {
if (action == ACTION_RESPOND_ALLOW) {
withReply:^(SNTAction action) {
if (action == SNTActionRespondAllow) {
std::cerr << "File exists in [whitelist] kernel cache" << std::endl;
;
} else if (action == ACTION_RESPOND_DENY) {
} else if (action == SNTActionRespondDeny) {
std::cerr << "File exists in [blacklist] kernel cache" << std::endl;
;
} else if (action == ACTION_UNSET) {
} else if (action == SNTActionUnset) {
std::cerr << "File does not exist in cache" << std::endl;
;
}

10
LICENSE
View File

@@ -200,3 +200,13 @@
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.
------------------
Files: Testing/integration/VM/*
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -7,6 +7,6 @@ whichever comes first.
To report vulnerabilities to us privately, please e-mail `santa-team@google.com`.
If you want to encrypt your e-mail, you can use our GPG key `0x92AFE41DAB49BBB6`
available on pool.sks-keyservers.net:
available on keyserver.ubuntu.com:
`gpg --keyserver pool.sks-keyservers.net --recv-key 0x92AFE41DAB49BBB6`
`gpg --keyserver keyserver.ubuntu.com --recv-key 0x92AFE41DAB49BBB6`

View File

@@ -31,19 +31,52 @@ cc_library(
],
)
objc_library(
name = "SystemResources",
srcs = ["SystemResources.mm"],
hdrs = ["SystemResources.h"],
deps = [
":SNTLogging",
],
)
cc_library(
name = "SantaCache",
hdrs = ["SantaCache.h"],
deps = ["//Source/common:SNTCommon"],
deps = [":BranchPrediction"],
)
santa_unit_test(
name = "SantaCacheTest",
srcs = [
"SantaCache.h",
"SantaCacheTest.mm",
srcs = ["SantaCacheTest.mm"],
deps = [
":SantaCache",
],
)
objc_library(
name = "BranchPrediction",
hdrs = ["BranchPrediction.h"],
)
objc_library(
name = "SantaVnode",
hdrs = ["SantaVnode.h"],
)
objc_library(
name = "Platform",
hdrs = ["Platform.h"],
)
objc_library(
name = "SantaVnodeHash",
srcs = ["SantaVnodeHash.mm"],
hdrs = ["SantaVnodeHash.h"],
deps = [
":SantaCache",
":SantaVnode",
],
deps = ["//Source/common:SNTCommon"],
)
objc_library(
@@ -74,11 +107,11 @@ objc_library(
objc_library(
name = "SNTCachedDecision",
srcs = ["SNTCachedDecision.m"],
srcs = ["SNTCachedDecision.mm"],
hdrs = ["SNTCachedDecision.h"],
deps = [
":SNTCommon",
":SNTCommonEnums",
":SantaVnode",
],
)
@@ -86,6 +119,10 @@ objc_library(
name = "SNTDeviceEvent",
srcs = ["SNTDeviceEvent.m"],
hdrs = ["SNTDeviceEvent.h"],
module_name = "santa_common_SNTDeviceEvent",
sdk_frameworks = [
"Foundation",
],
deps = [
":SNTCommonEnums",
],
@@ -100,6 +137,10 @@ objc_library(
name = "SNTConfigurator",
srcs = ["SNTConfigurator.m"],
hdrs = ["SNTConfigurator.h"],
module_name = "santa_common_SNTConfigurator",
sdk_frameworks = [
"Foundation",
],
deps = [
":SNTCommonEnums",
":SNTRule",
@@ -142,15 +183,6 @@ objc_library(
],
)
cc_library(
name = "SNTCommon",
hdrs = ["SNTCommon.h"],
defines = [
"TARGET_OS_OSX",
"TARGET_OS_MAC",
],
)
objc_library(
name = "SNTLogging",
srcs = ["SNTLogging.m"],
@@ -158,18 +190,27 @@ objc_library(
deps = [":SNTConfigurator"],
)
cc_library(
name = "SNTPrefixTree",
srcs = ["SNTPrefixTree.cc"],
hdrs = ["SNTPrefixTree.h"],
copts = ["-std=c++11"],
deps = [":SNTLogging"],
objc_library(
name = "PrefixTree",
hdrs = ["PrefixTree.h"],
deps = [
":SNTLogging",
"@com_google_absl//absl/synchronization",
],
)
objc_library(
name = "Unit",
hdrs = ["Unit.h"],
)
objc_library(
name = "SNTRule",
srcs = ["SNTRule.m"],
hdrs = ["SNTRule.h"],
sdk_frameworks = [
"Foundation",
],
deps = [
":SNTCommonEnums",
":SNTSyncConstants",
@@ -201,13 +242,19 @@ objc_library(
name = "SNTSyncConstants",
srcs = ["SNTSyncConstants.m"],
hdrs = ["SNTSyncConstants.h"],
sdk_frameworks = [
"Foundation",
],
)
objc_library(
name = "SNTSystemInfo",
srcs = ["SNTSystemInfo.m"],
hdrs = ["SNTSystemInfo.h"],
sdk_frameworks = ["IOKit"],
sdk_frameworks = [
"Foundation",
"IOKit",
],
)
objc_library(
@@ -281,11 +328,11 @@ objc_library(
srcs = ["SNTXPCUnprivilegedControlInterface.m"],
hdrs = ["SNTXPCUnprivilegedControlInterface.h"],
deps = [
":SNTCommon",
":SNTCommonEnums",
":SNTRule",
":SNTStoredEvent",
":SNTXPCBundleServiceInterface",
":SantaVnode",
"@MOLCertificate",
"@MOLXPCConnection",
],
@@ -307,9 +354,9 @@ santa_unit_test(
)
santa_unit_test(
name = "SNTPrefixTreeTest",
srcs = ["SNTPrefixTreeTest.mm"],
deps = [":SNTPrefixTree"],
name = "PrefixTreeTest",
srcs = ["PrefixTreeTest.mm"],
deps = [":PrefixTree"],
)
santa_unit_test(
@@ -331,11 +378,11 @@ santa_unit_test(
test_suite(
name = "unit_tests",
tests = [
":PrefixTreeTest",
":SNTCachedDecisionTest",
":SNTFileInfoTest",
":SNTKVOManagerTest",
":SNTMetricSetTest",
":SNTPrefixTreeTest",
":SNTRuleTest",
":SantaCacheTest",
],
@@ -351,6 +398,7 @@ objc_library(
"bsm",
],
deps = [
":SystemResources",
"@OCMock",
"@com_google_googletest//:gtest",
],

View File

@@ -0,0 +1,22 @@
/// Copyright 2022 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__BRANCHPREDICTION_H
#define SANTA__COMMON__BRANCHPREDICTION_H
// Helpful macros to use when the the outcome is largely known
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
#endif

34
Source/common/Platform.h Normal file
View File

@@ -0,0 +1,34 @@
/// Copyright 2022 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__PLATFORM_H
#define SANTA__COMMON__PLATFORM_H
#include <Availability.h>
#if defined(MAC_OS_VERSION_12_0) && \
MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_12_0
#define HAVE_MACOS_12 1
#else
#define HAVE_MACOS_12 0
#endif
#if defined(MAC_OS_VERSION_13_0) && \
MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_13_0
#define HAVE_MACOS_13 1
#else
#define HAVE_MACOS_13 0
#endif
#endif

302
Source/common/PrefixTree.h Normal file
View File

@@ -0,0 +1,302 @@
/// Copyright 2022 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__PREFIXTREE_H
#define SANTA__COMMON__PREFIXTREE_H
#include <sys/syslimits.h>
#include <optional>
#import "Source/common/SNTLogging.h"
#include "absl/synchronization/mutex.h"
#if SANTA_PREFIX_TREE_DEBUG
#define DEBUG_LOG LOGD
#else
#define DEBUG_LOG(format, ...) // NOP
#endif
namespace santa::common {
template <typename ValueT>
class PrefixTree {
private:
// Forward declaration
enum class NodeType;
class TreeNode;
public:
PrefixTree(uint32_t max_depth = PATH_MAX)
: root_(new TreeNode()), max_depth_(max_depth), node_count_(0) {}
~PrefixTree() { PruneLocked(root_); }
bool InsertPrefix(const char *s, ValueT value) {
absl::MutexLock lock(&lock_);
return InsertLocked(s, value, NodeType::kPrefix);
}
bool InsertLiteral(const char *s, ValueT value) {
absl::MutexLock lock(&lock_);
return InsertLocked(s, value, NodeType::kLiteral);
}
bool HasPrefix(const char *input) {
absl::ReaderMutexLock lock(&lock_);
return HasPrefixLocked(input);
}
std::optional<ValueT> LookupLongestMatchingPrefix(const char *input) {
if (!input) {
return std::nullopt;
}
absl::ReaderMutexLock lock(&lock_);
return LookupLongestMatchingPrefixLocked(input);
}
void Reset() {
absl::MutexLock lock(&lock_);
PruneLocked(root_);
root_ = new TreeNode();
node_count_ = 0;
}
#if SANTA_PREFIX_TREE_DEBUG
void Print() {
char buf[max_depth_ + 1];
memset(buf, 0, sizeof(buf));
absl::ReaderMutexLock lock(&lock_);
PrintLocked(root_, buf, 0);
}
uint32_t NodeCount() {
absl::ReaderMutexLock lock(&lock_);
return node_count_;
}
#endif
private:
ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_)
bool InsertLocked(const char *input, ValueT value, NodeType node_type) {
const char *p = input;
TreeNode *node = root_;
while (*p) {
uint8_t cur_byte = (uint8_t)*p;
TreeNode *child_node = node->children_[cur_byte];
if (!child_node) {
// Current node doesn't exist...
// Create the rest of the nodes in the tree for the given string
// Keep a pointer to where this new branch starts from. If the
// input length exceeds max_depth, the new branch will need to
// be pruned.
TreeNode *branch_start_node = node;
uint8_t branch_start_byte = (uint8_t)*p;
do {
TreeNode *new_node = new TreeNode();
node->children_[cur_byte] = new_node;
node = new_node;
node_count_++;
// Check current depth...
if (p - input >= max_depth_) {
// Attempted to add a string that exceeded max depth
// Prune tree from start of this new branch
PruneLocked(branch_start_node->children_[branch_start_byte]);
branch_start_node->children_[branch_start_byte] = nullptr;
return false;
}
cur_byte = (uint8_t) * ++p;
} while (*p);
node->node_type_ = node_type;
node->value_ = value;
return true;
} else if (*(p + 1) == '\0') {
// Current node exists and we're at the end of our input...
// Note: The current node's data will be overwritten
// Only increment node count if the previous node type wasn't already a
// prefix or literal type (in which case it was already counted)
if (child_node->node_type_ == NodeType::kInner) {
node_count_++;
}
child_node->node_type_ = node_type;
child_node->value_ = value;
return true;
}
node = child_node;
p++;
}
// Should only get here when input is an empty string
return false;
}
ABSL_SHARED_LOCKS_REQUIRED(lock_)
bool HasPrefixLocked(const char *input) {
TreeNode *node = root_;
const char *p = input;
while (*p) {
node = node->children_[(uint8_t)*p++];
if (!node) {
break;
}
if (node->node_type_ == NodeType::kPrefix ||
(*p == '\0' && node->node_type_ == NodeType::kLiteral)) {
return true;
}
}
return false;
}
ABSL_SHARED_LOCKS_REQUIRED(lock_)
std::optional<ValueT> LookupLongestMatchingPrefixLocked(const char *input) {
TreeNode *node = root_;
TreeNode *match = nullptr;
const char *p = input;
while (*p) {
node = node->children_[(uint8_t)*p++];
if (!node) {
break;
}
if (node->node_type_ == NodeType::kPrefix ||
(*p == '\0' && node->node_type_ == NodeType::kLiteral)) {
match = node;
}
}
return match ? std::make_optional<ValueT>(match->value_) : std::nullopt;
}
ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_)
void PruneLocked(TreeNode *target) {
if (!target) {
return;
}
// For deep trees, a recursive approach will generate too many stack frames.
// Since the depth of the tree is configurable, err on the side of caution
// and use a "stack" to walk the tree in a non-recursive manner.
TreeNode **stack = new TreeNode *[node_count_ + 1];
if (!stack) {
LOGE(@"Unable to prune tree!");
return;
}
uint32_t count = 0;
// Seed the "stack" with a starting node.
stack[count++] = target;
// Start at the target node and walk the tree to find and delete all the
// sub-nodes.
while (count) {
TreeNode *node = stack[--count];
for (int i = 0; i < 256; ++i) {
if (!node->children_[i]) {
continue;
}
stack[count++] = node->children_[i];
}
delete node;
--node_count_;
}
delete[] stack;
}
#if SANTA_PREFIX_TREE_DEBUG
ABSL_SHARED_LOCKS_REQUIRED(lock_)
void PrintLocked(TreeNode *node, char *buf, uint32_t depth) {
for (size_t i = 0; i < 256; i++) {
TreeNode *cur_node = node->children_[i];
if (cur_node) {
buf[depth] = i;
if (cur_node->node_type_ != NodeType::kInner) {
printf("\t%s (type: %s)\n", buf,
cur_node->node_type_ == NodeType::kPrefix ? "prefix" : "literal");
}
PrintLocked(cur_node, buf, depth + 1);
buf[depth] = '\0';
}
}
}
#endif
enum class NodeType {
kInner = 0,
kPrefix,
kLiteral,
};
///
/// TreeNode is a wrapper class that represents one byte.
/// 1 node can represent a whole ASCII character.
/// For example a pointer to the 'A' node will be stored at children[0x41].
/// It takes 1-4 nodes to represent a UTF-8 encoded Unicode character.
///
/// The path for "/🤘" would look like this:
/// children[0x2f] -> children[0xf0] -> children[0x9f] -> children[0xa4]
/// -> children[0x98]
///
/// The path for "/dev" is:
/// children[0x2f] -> children[0x64] -> children[0x65] -> children[0x76]
///
/// Lookups of children are O(1).
///
/// Having the nodes represented by a smaller width, such as a nibble (1/2
/// byte), would drastically decrease the memory footprint but would double
/// required dereferences.
///
/// TODO(bur): Potentially convert this into a full on radix tree.
///
class TreeNode {
public:
TreeNode() : children_(), node_type_(NodeType::kInner) {}
~TreeNode() = default;
TreeNode *children_[256];
PrefixTree::NodeType node_type_;
ValueT value_;
};
TreeNode *root_;
const uint32_t max_depth_;
uint32_t node_count_ ABSL_GUARDED_BY(lock_);
absl::Mutex lock_;
};
} // namespace santa::common
#endif

View File

@@ -0,0 +1,224 @@
/// Copyright 2022 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 <XCTest/XCTest.h>
#define SANTA_PREFIX_TREE_DEBUG 1
#include "Source/common/PrefixTree.h"
using santa::common::PrefixTree;
@interface PrefixTreeTest : XCTestCase
@end
@implementation PrefixTreeTest
- (void)testBasic {
PrefixTree<int> tree;
XCTAssertFalse(tree.HasPrefix("/foo/bar/baz"));
XCTAssertFalse(tree.HasPrefix("/foo/bar.txt"));
XCTAssertFalse(tree.HasPrefix("/baz"));
XCTAssertTrue(tree.InsertPrefix("/foo", 12));
XCTAssertTrue(tree.InsertPrefix("/bar", 34));
XCTAssertTrue(tree.InsertLiteral("/foo/bar", 56));
// Re-inserting something that exists is allowed
XCTAssertTrue(tree.InsertLiteral("/foo", 78));
XCTAssertTrue(tree.InsertPrefix("/foo", 56));
XCTAssertTrue(tree.HasPrefix("/foo/bar/baz"));
XCTAssertTrue(tree.HasPrefix("/foo/bar.txt"));
XCTAssertFalse(tree.HasPrefix("/baz"));
// Empty strings are not supported
XCTAssertFalse(tree.InsertLiteral("", 0));
XCTAssertFalse(tree.InsertPrefix("", 0));
}
- (void)testHasPrefix {
PrefixTree<int> tree;
XCTAssertTrue(tree.InsertPrefix("/foo", 0));
XCTAssertTrue(tree.InsertLiteral("/bar", 0));
XCTAssertTrue(tree.InsertLiteral("/baz", 0));
XCTAssertTrue(tree.InsertLiteral("/qaz", 0));
// Check that a tree with a matching prefix is successful
XCTAssertTrue(tree.HasPrefix("/foo.txt"));
// This shouldn't succeed because `/bar` `/baz` and `qaz` are literals
XCTAssertFalse(tree.HasPrefix("/bar.txt"));
XCTAssertFalse(tree.HasPrefix("/baz.txt"));
XCTAssertFalse(tree.HasPrefix("/qaz.txt"));
// Now change `/bar` to a prefix type and retest HasPrefix
// `/bar.txt` should now succeed, but `/baz.txt` should still not pass
XCTAssertTrue(tree.InsertPrefix("/bar", 0));
XCTAssertTrue(tree.HasPrefix("/bar.txt"));
XCTAssertFalse(tree.HasPrefix("/baz.txt"));
XCTAssertFalse(tree.HasPrefix("/qaz.txt"));
// Insert a new prefix string to allow `/baz.txt` to have a valid prefix
XCTAssertTrue(tree.InsertPrefix("/b", 0));
XCTAssertTrue(tree.HasPrefix("/baz.txt"));
XCTAssertFalse(tree.HasPrefix("/qaz.txt"));
// An exact match on a literal allows HasPrefix to succeed
XCTAssertTrue(tree.InsertLiteral("/qaz.txt", 0));
XCTAssertTrue(tree.HasPrefix("/qaz.txt"));
}
- (void)testLookupLongestMatchingPrefix {
PrefixTree<int> tree;
XCTAssertTrue(tree.InsertPrefix("/foo", 12));
XCTAssertTrue(tree.InsertPrefix("/bar", 34));
XCTAssertTrue(tree.InsertPrefix("/foo/bar.txt", 56));
std::optional<int> value;
// Matching exact prefix
value = tree.LookupLongestMatchingPrefix("/foo");
XCTAssertEqual(value.value_or(0), 12);
// Ensure changing node type works as expected
// Literals must match exactly.
value = tree.LookupLongestMatchingPrefix("/foo/bar.txt.tmp");
XCTAssertEqual(value.value_or(0), 56);
XCTAssertTrue(tree.InsertLiteral("/foo/bar.txt", 90));
value = tree.LookupLongestMatchingPrefix("/foo/bar.txt.tmp");
XCTAssertEqual(value.value_or(0), 12);
// Inserting over an exiting node returns the new value
XCTAssertTrue(tree.InsertPrefix("/foo", 78));
value = tree.LookupLongestMatchingPrefix("/foo");
XCTAssertEqual(value.value_or(0), 78);
// No matching prefix
value = tree.LookupLongestMatchingPrefix("/asdf");
XCTAssertEqual(value.value_or(0), 0);
}
- (void)testNodeCounts {
const uint32_t maxDepth = 100;
PrefixTree<int> tree(100);
XCTAssertEqual(tree.NodeCount(), 0);
// Start with a small string
XCTAssertTrue(tree.InsertPrefix("asdf", 0));
XCTAssertEqual(tree.NodeCount(), 4);
// Add a couple more characters to the existing string
XCTAssertTrue(tree.InsertPrefix("asdfgh", 0));
XCTAssertEqual(tree.NodeCount(), 6);
// Inserting a string that exceeds max depth doesn't increase node count
XCTAssertFalse(tree.InsertPrefix(std::string(maxDepth + 10, 'A').c_str(), 0));
XCTAssertEqual(tree.NodeCount(), 6);
// Add a new string that is a prefix of an existing string
// This should increment the count by one since a new terminal node exists
XCTAssertTrue(tree.InsertPrefix("as", 0));
XCTAssertEqual(tree.NodeCount(), 7);
// Re-inserting onto an existing node shouldn't modify the count
tree.InsertLiteral("as", 0);
tree.InsertPrefix("as", 0);
XCTAssertEqual(tree.NodeCount(), 7);
}
- (void)testReset {
// Ensure resetting a tree removes all content
PrefixTree<int> tree;
tree.Reset();
XCTAssertEqual(tree.NodeCount(), 0);
XCTAssertTrue(tree.InsertPrefix("asdf", 0));
XCTAssertTrue(tree.InsertPrefix("qwerty", 0));
XCTAssertTrue(tree.HasPrefix("asdf"));
XCTAssertTrue(tree.HasPrefix("qwerty"));
XCTAssertEqual(tree.NodeCount(), 10);
tree.Reset();
XCTAssertFalse(tree.HasPrefix("asdf"));
XCTAssertFalse(tree.HasPrefix("qwerty"));
XCTAssertEqual(tree.NodeCount(), 0);
}
- (void)testComplexValues {
class Foo {
public:
Foo(int x) : x_(x) {}
int X() { return x_; }
private:
int x_;
};
PrefixTree<std::shared_ptr<Foo>> tree;
XCTAssertTrue(tree.InsertPrefix("foo", std::make_shared<Foo>(123)));
XCTAssertTrue(tree.InsertPrefix("bar", std::make_shared<Foo>(456)));
std::optional<std::shared_ptr<Foo>> value;
value = tree.LookupLongestMatchingPrefix("foo");
XCTAssertTrue(value.has_value() && value->get()->X() == 123);
value = tree.LookupLongestMatchingPrefix("bar");
XCTAssertTrue(value.has_value() && value->get()->X() == 456);
value = tree.LookupLongestMatchingPrefix("asdf");
XCTAssertFalse(value.has_value());
}
- (void)testThreading {
uint32_t count = 4096;
auto t = new PrefixTree<int>(count * (uint32_t)[NSUUID UUID].UUIDString.length);
__block NSMutableArray *UUIDs = [NSMutableArray arrayWithCapacity:count];
for (int i = 0; i < count; ++i) {
[UUIDs addObject:[NSUUID UUID].UUIDString];
}
__block _Atomic BOOL stop = NO;
// Create a bunch of background noise.
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (uint64_t i = 0; i < UINT64_MAX; ++i) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
t->HasPrefix([UUIDs[i % count] UTF8String]);
});
if (stop) return;
}
});
// Fill up the tree.
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t i) {
XCTAssertEqual(t->InsertPrefix([UUIDs[i] UTF8String], 0), true);
});
// Make sure every leaf byte is found.
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t i) {
XCTAssertTrue(t->HasPrefix([UUIDs[i] UTF8String]));
});
stop = YES;
}
@end

View File

@@ -15,8 +15,8 @@
#import <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
#import "Source/common/SNTCommon.h"
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SantaVnode.h"
@class MOLCertificate;
@@ -27,7 +27,7 @@
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile;
@property santa_vnode_id_t vnodeId;
@property SantaVnode vnodeId;
@property SNTEventState decision;
@property NSString *decisionExtra;
@property NSString *sha256;

View File

@@ -1,3 +1,4 @@
/// Copyright 2015-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,8 +20,7 @@
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile {
self = [super init];
if (self) {
_vnodeId.fsid = (uint64_t)esFile->stat.st_dev;
_vnodeId.fileid = esFile->stat.st_ino;
_vnodeId = SantaVnode::VnodeForFile(esFile);
}
return self;
}

View File

@@ -1,59 +0,0 @@
/// Copyright 2015-2022 Google Inc. All rights reserved.
///
/// 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
///
/// http://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.
///
/// Common defines between daemon <-> client
///
#ifndef SANTA__COMMON__COMMON_H
#define SANTA__COMMON__COMMON_H
#include <stdint.h>
#include <sys/param.h>
// Branch prediction
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
typedef enum {
ACTION_UNSET,
// REQUESTS
// If an operation is awaiting a cache decision from a similar operation
// currently being processed, it will poll about every 5 ms for an answer.
ACTION_REQUEST_BINARY,
// RESPONSES
ACTION_RESPOND_ALLOW,
ACTION_RESPOND_DENY,
ACTION_RESPOND_ALLOW_COMPILER,
} santa_action_t;
#define RESPONSE_VALID(x) \
(x == ACTION_RESPOND_ALLOW || x == ACTION_RESPOND_DENY || \
x == ACTION_RESPOND_ALLOW_COMPILER)
// Struct to manage vnode IDs
typedef struct santa_vnode_id_t {
uint64_t fsid;
uint64_t fileid;
#ifdef __cplusplus
bool operator==(const santa_vnode_id_t &rhs) const {
return fsid == rhs.fsid && fileid == rhs.fileid;
}
#endif
} santa_vnode_id_t;
#endif // SANTA__COMMON__COMMON_H

View File

@@ -19,6 +19,23 @@
/// The integer values are also stored in the database and so shouldn't be changed.
///
typedef NS_ENUM(NSInteger, SNTAction) {
SNTActionUnset,
// REQUESTS
// If an operation is awaiting a cache decision from a similar operation
// currently being processed, it will poll about every 5 ms for an answer.
SNTActionRequestBinary,
// RESPONSES
SNTActionRespondAllow,
SNTActionRespondDeny,
SNTActionRespondAllowCompiler,
};
#define RESPONSE_VALID(x) \
(x == SNTActionRespondAllow || x == SNTActionRespondDeny || x == SNTActionRespondAllowCompiler)
typedef NS_ENUM(NSInteger, SNTRuleType) {
SNTRuleTypeUnknown,
@@ -118,6 +135,17 @@ typedef NS_ENUM(NSInteger, SNTMetricFormatType) {
SNTMetricFormatTypeMonarchJSON,
};
#ifdef __cplusplus
enum class FileAccessPolicyDecision {
kNoPolicy,
kDenied,
kDeniedInvalidSignature,
kAllowed,
kAllowedReadAccess,
kAllowedAuditOnly,
};
#endif
static const char *kSantaDPath =
"/Applications/Santa.app/Contents/Library/SystemExtensions/"
"com.google.santa.daemon.systemextension/Contents/MacOS/com.google.santa.daemon";

View File

@@ -194,6 +194,13 @@
///
@property(readonly, nonatomic) SNTEventLogType eventLogType;
///
/// Returns the raw value of the EventLogType configuration key instead of being
/// converted to the SNTEventLogType enum. If the key is not set, the default log
/// type is returned.
///
@property(readonly, nonatomic) NSString *eventLogTypeRaw;
///
/// If eventLogType is set to Filelog, eventLogPath will provide the path to save logs.
/// Defaults to /var/db/santa/santa.log.
@@ -238,6 +245,33 @@
///
@property(readonly, nonatomic) float spoolDirectoryEventMaxFlushTimeSec;
///
/// If set, contains the filesystem access policy configuration.
///
/// @note: The property fileAccessPolicyPlist will be ignored if
/// fileAccessPolicy is set.
/// @note: This property is KVO compliant.
///
@property(readonly, nonatomic) NSDictionary *fileAccessPolicy;
///
/// If set, contains the path to the filesystem access policy config plist.
///
/// @note: This property will be ignored if fileAccessPolicy is set.
/// @note: This property is KVO compliant.
///
@property(readonly, nonatomic) NSString *fileAccessPolicyPlist;
///
/// If fileAccessPolicyPlist is set, fileAccessPolicyUpdateIntervalSec
/// sets the number of seconds between times that the configuration file is
/// re-read and policies reconstructed.
/// Defaults to 600 seconds (10 minutes)
///
/// @note: This property is KVO compliant, but should only be read once at santad startup.
///
@property(readonly, nonatomic) uint32_t fileAccessPolicyUpdateIntervalSec;
///
/// 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.

View File

@@ -45,6 +45,10 @@
/// The hard-coded path to the sync state file.
NSString *const kSyncStateFilePath = @"/var/db/santa/sync-state.plist";
#ifdef DEBUG
NSString *const kConfigOverrideFilePath = @"/var/db/santa/config-overrides.plist";
#endif
/// The domain used by mobileconfig.
static NSString *const kMobileConfigDomain = @"com.google.santa";
@@ -93,6 +97,10 @@ static NSString *const kSpoolDirectoryFileSizeThresholdKB = @"SpoolDirectoryFile
static NSString *const kSpoolDirectorySizeThresholdMB = @"SpoolDirectorySizeThresholdMB";
static NSString *const kSpoolDirectoryEventMaxFlushTimeSec = @"SpoolDirectoryEventMaxFlushTimeSec";
static NSString *const kFileAccessPolicy = @"FileAccessPolicy";
static NSString *const kFileAccessPolicyPlist = @"FileAccessPolicyPlist";
static NSString *const kFileAccessPolicyUpdateIntervalSec = @"FileAccessPolicyUpdateIntervalSec";
static NSString *const kEnableMachineIDDecoration = @"EnableMachineIDDecoration";
static NSString *const kEnableForkAndExitLogging = @"EnableForkAndExitLogging";
@@ -204,6 +212,9 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kSpoolDirectoryFileSizeThresholdKB : number,
kSpoolDirectorySizeThresholdMB : number,
kSpoolDirectoryEventMaxFlushTimeSec : number,
kFileAccessPolicy : dictionary,
kFileAccessPolicyPlist : string,
kFileAccessPolicyUpdateIntervalSec : number,
kEnableMachineIDDecoration : number,
kEnableForkAndExitLogging : number,
kIgnoreOtherEndpointSecurityClients : number,
@@ -233,7 +244,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
#pragma mark Singleton retriever
+ (instancetype)configurator {
// The returned value is marked unsafe_unretained to avoid unnecessary retain/release handling.
// The object returned is guaranteed to exist for the lifetime of the process so there's no need
// to do this handling.
+ (__unsafe_unretained instancetype)configurator {
static SNTConfigurator *sharedConfigurator;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@@ -407,6 +421,18 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicy {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicyPlist {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicyUpdateIntervalSec {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingEnableMachineIDDecoration {
return [self configStateSet];
}
@@ -759,6 +785,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
}
}
- (NSString *)eventLogTypeRaw {
return self.configState[kEventLogType] ?: @"file";
}
- (NSString *)eventLogPath {
return self.configState[kEventLogPath] ?: @"/var/db/santa/santa.log";
}
@@ -785,6 +815,25 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
: 15.0;
}
- (NSDictionary *)fileAccessPolicy {
return self.configState[kFileAccessPolicy];
}
- (NSString *)fileAccessPolicyPlist {
// This property is ignored when kFileAccessPolicy is set
if (self.configState[kFileAccessPolicy]) {
return nil;
} else {
return self.configState[kFileAccessPolicyPlist];
}
}
- (uint32_t)fileAccessPolicyUpdateIntervalSec {
return self.configState[kFileAccessPolicyUpdateIntervalSec]
? [self.configState[kFileAccessPolicyUpdateIntervalSec] unsignedIntValue]
: 60 * 10;
}
- (BOOL)enableMachineIDDecoration {
NSNumber *number = self.configState[kEnableMachineIDDecoration];
return number ? [number boolValue] : NO;
@@ -992,6 +1041,18 @@ 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]]) continue;
forcedConfig[key] = obj;
if (self.forcedConfigKeyTypes[key] == [NSRegularExpression class]) {
NSString *pattern = [obj isKindOfClass:[NSString class]] ? obj : nil;
forcedConfig[key] = [self expressionForPattern:pattern];
}
}
#endif
return forcedConfig;
}
@@ -1008,12 +1069,50 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
selector:@selector(defaultsChanged:)
name:NSUserDefaultsDidChangeNotification
object:nil];
#ifdef DEBUG
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
[self watchOverridesFile];
});
#endif
}
#ifdef DEBUG
- (void)watchOverridesFile {
while (![[NSFileManager defaultManager] fileExistsAtPath:kConfigOverrideFilePath]) {
[NSThread sleepForTimeInterval:0.2];
}
[self defaultsChanged:nil];
int descriptor = open([kConfigOverrideFilePath fileSystemRepresentation], O_EVTONLY);
if (descriptor < 0) {
return;
}
dispatch_source_t source =
dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, descriptor,
DISPATCH_VNODE_WRITE | DISPATCH_VNODE_RENAME | DISPATCH_VNODE_DELETE,
dispatch_get_global_queue(QOS_CLASS_UTILITY, 0));
dispatch_source_set_event_handler(source, ^{
dispatch_async(dispatch_get_main_queue(), ^{
[self defaultsChanged:nil];
});
unsigned long events = dispatch_source_get_data(source);
if ((events & DISPATCH_VNODE_DELETE) || (events & DISPATCH_VNODE_RENAME)) {
dispatch_source_cancel(source);
}
});
dispatch_source_set_cancel_handler(source, ^{
close(descriptor);
[self watchOverridesFile];
});
dispatch_resume(source);
}
#endif
- (void)defaultsChanged:(void *)v {
SEL handleChange = @selector(handleChange);
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:handleChange object:nil];
[self performSelector:handleChange withObject:nil afterDelay:5.0f];
[self performSelector:handleChange withObject:nil afterDelay:1.0f];
}
///
@@ -1036,6 +1135,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
for (id rule in staticRules) {
if (![rule isKindOfClass:[NSDictionary class]]) return;
SNTRule *r = [[SNTRule alloc] initWithDictionary:rule];
if (!r) continue;
rules[r.identifier] = r;
}
self.cachedStaticRules = [rules copy];

View File

@@ -572,6 +572,10 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
NSData *cmdData = [self safeSubdataWithRange:NSMakeRange(offset, sz_segment)];
if (!cmdData) return nil;
if (((struct load_command *)[cmdData bytes])->cmdsize < sizeof(struct load_command)) {
return nil;
}
if (is64) {
struct segment_command_64 *lc = (struct segment_command_64 *)[cmdData bytes];
if (lc->cmd == LC_SEGMENT_64 && memcmp(lc->segname, "__TEXT", 6) == 0) {
@@ -642,7 +646,10 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
///
- (NSData *)safeSubdataWithRange:(NSRange)range {
@try {
if ((range.location + range.length) > self.fileSize) return nil;
NSUInteger size;
if (__builtin_add_overflow(range.location, range.length, &size) || size > self.fileSize) {
return nil;
}
[self.fileHandle seekToFileOffset:range.location];
NSData *d = [self.fileHandle readDataOfLength:range.length];
if (d.length != range.length) return nil;

View File

@@ -34,7 +34,12 @@
- (void)testPathStandardizing {
SNTFileInfo *sut = [[SNTFileInfo alloc] initWithPath:@"/Applications/Safari.app"];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.path, @"/Applications/Safari.app/Contents/MacOS/Safari");
if (@available(macOS 13.0, *)) {
XCTAssertEqualObjects(sut.path, @"/System/Volumes/Preboot/Cryptexes/App/System/Applications/"
@"Safari.app/Contents/MacOS/Safari");
} else {
XCTAssertEqualObjects(sut.path, @"/Applications/Safari.app/Contents/MacOS/Safari");
}
sut = [[SNTFileInfo alloc] initWithPath:@"../../../../../../../../../../../../../../../bin/ls"];
XCTAssertEqualObjects(sut.path, @"/bin/ls");
@@ -90,6 +95,11 @@
}
- (void)testKext {
// Skip this test on macOS 13 as KEXTs have moved into the kernelcache.
if (@available(macOS 13.0, *)) {
return;
}
SNTFileInfo *sut = [[SNTFileInfo alloc]
initWithPath:@"/System/Library/Extensions/AppleAPIC.kext/Contents/MacOS/AppleAPIC"];

View File

@@ -1,227 +0,0 @@
/// Copyright 2018 Google Inc. All rights reserved.
///
/// 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
///
/// http://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/common/SNTPrefixTree.h"
#include <string.h>
#include <mutex>
#define LOGD(format, ...) // NOP
#define LOGE(format, ...) // NOP
#define lck_rw_lock_shared(l) pthread_rwlock_rdlock(&l)
#define lck_rw_unlock_shared(l) pthread_rwlock_unlock(&l)
#define lck_rw_lock_exclusive(l) pthread_rwlock_wrlock(&l)
#define lck_rw_unlock_exclusive(l) pthread_rwlock_unlock(&l)
#define lck_rw_lock_shared_to_exclusive(l) \
({ \
pthread_rwlock_unlock(&l); \
false; \
})
#define lck_rw_lock_exclusive_to_shared(l) \
({ \
pthread_rwlock_unlock(&l); \
pthread_rwlock_rdlock(&l); \
})
#define lck_mtx_lock(l) l->lock()
#define lck_mtx_unlock(l) l->unlock()
SNTPrefixTree::SNTPrefixTree(uint32_t max_nodes) {
root_ = new SantaPrefixNode();
node_count_ = 0;
max_nodes_ = max_nodes;
pthread_rwlock_init(&spt_lock_, nullptr);
spt_add_lock_ = new std::mutex;
}
IOReturn SNTPrefixTree::AddPrefix(const char *prefix, uint64_t *node_count) {
// Serialize requests to AddPrefix. Otherwise one AddPrefix thread could
// overwrite whole branches of another. HasPrefix is still free to read the
// tree, until AddPrefix needs to modify it.
lck_mtx_lock(spt_add_lock_);
// Don't allow an empty prefix.
if (prefix[0] == '\0') return kIOReturnBadArgument;
LOGD("Trying to add prefix: %s", prefix);
// Enforce max tree depth.
size_t len = strnlen(prefix, max_nodes_);
// Grab a shared lock until a new branch is required.
lck_rw_lock_shared(spt_lock_);
SantaPrefixNode *node = root_;
for (size_t i = 0; i < len; ++i) {
// If there is a node in the path that is considered a prefix, stop adding.
// For our purposes we only care about the shortest path that matches.
if (node->isPrefix) break;
// Only process a byte at a time.
uint8_t value = (uint8_t)prefix[i];
// Create the child if it does not exist.
if (!node->children[value]) {
// Upgrade the shared lock.
// If the upgrade fails, the shared lock is released.
if (!lck_rw_lock_shared_to_exclusive(spt_lock_)) {
// Grab a new exclusive lock.
lck_rw_lock_exclusive(spt_lock_);
}
// Is there enough room for the rest of the prefix?
if ((node_count_ + (len - i)) > max_nodes_) {
LOGE("Prefix tree is full, can not add: %s", prefix);
if (node_count) *node_count = node_count_;
lck_rw_unlock_exclusive(spt_lock_);
lck_mtx_unlock(spt_add_lock_);
return kIOReturnNoResources;
}
// Create the rest of the prefix.
while (i < len) {
value = (uint8_t)prefix[i++];
SantaPrefixNode *new_node = new SantaPrefixNode();
node->children[value] = new_node;
++node_count_;
node = new_node;
}
// This is the end, mark the node as a prefix.
LOGD("Added prefix: %s", prefix);
node->isPrefix = true;
// Downgrade the exclusive lock
lck_rw_lock_exclusive_to_shared(spt_lock_);
} else if (i + 1 == len) {
// If the child does exist and it is the end...
// Set the new, higher prefix and prune the now dead nodes.
if (!lck_rw_lock_shared_to_exclusive(spt_lock_)) {
lck_rw_lock_exclusive(spt_lock_);
}
PruneNode(node->children[value]);
SantaPrefixNode *new_node = new SantaPrefixNode();
new_node->isPrefix = true;
node->children[value] = new_node;
++node_count_;
LOGD("Added prefix: %s", prefix);
lck_rw_lock_exclusive_to_shared(spt_lock_);
}
// Get ready for the next iteration.
node = node->children[value];
}
if (node_count) *node_count = node_count_;
lck_rw_unlock_shared(spt_lock_);
lck_mtx_unlock(spt_add_lock_);
return kIOReturnSuccess;
}
bool SNTPrefixTree::HasPrefix(const char *string) {
lck_rw_lock_shared(spt_lock_);
auto found = false;
SantaPrefixNode *node = root_;
// A well formed tree will always break this loop. Even if string doesn't
// terminate.
const char *p = string;
while (*p) {
// Only process a byte at a time.
node = node->children[(uint8_t)*p++];
// If it doesn't exist in the tree, no match.
if (!node) break;
// If it does exist, is it a prefix?
if (node->isPrefix) {
found = true;
break;
}
}
lck_rw_unlock_shared(spt_lock_);
return found;
}
void SNTPrefixTree::Reset() {
lck_rw_lock_exclusive(spt_lock_);
PruneNode(root_);
root_ = new SantaPrefixNode();
node_count_ = 0;
lck_rw_unlock_exclusive(spt_lock_);
}
void SNTPrefixTree::PruneNode(SantaPrefixNode *target) {
if (!target) return;
// For deep trees, a recursive approach will generate too many stack frames.
// Make a "stack" and walk the tree.
auto stack = new SantaPrefixNode *[node_count_ + 1];
if (!stack) {
LOGE("Unable to prune tree!");
return;
}
auto count = 0;
// Seed the "stack" with a starting node.
stack[count++] = target;
// Start at the target node and walk the tree to find and delete all the
// sub-nodes.
while (count) {
auto node = stack[--count];
for (int i = 0; i < 256; ++i) {
if (!node->children[i]) continue;
stack[count++] = node->children[i];
}
delete node;
--node_count_;
}
delete[] stack;
}
SNTPrefixTree::~SNTPrefixTree() {
lck_rw_lock_exclusive(spt_lock_);
PruneNode(root_);
root_ = nullptr;
lck_rw_unlock_exclusive(spt_lock_);
pthread_rwlock_destroy(&spt_lock_);
}

View File

@@ -1,91 +0,0 @@
/// Copyright 2018 Google Inc. All rights reserved.
///
/// 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
///
/// http://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__SANTA_DRIVER__SANTAPREFIXTREE_H
#define SANTA__SANTA_DRIVER__SANTAPREFIXTREE_H
#include <IOKit/IOReturn.h>
#include <sys/param.h>
// Support for unit testing.
#include <pthread.h>
#include <stdint.h>
#include <mutex>
///
/// SantaPrefixTree is a simple prefix tree implementation.
/// Operations are thread safe.
///
class SNTPrefixTree {
public:
// Add a prefix to the tree.
// Optionally pass node_count to get the number of nodes after the add.
IOReturn AddPrefix(const char *, uint64_t *node_count = nullptr);
// Check if the tree has a prefix for string.
bool HasPrefix(const char *string);
// Reset the tree.
void Reset();
SNTPrefixTree(uint32_t max_nodes = kDefaultMaxNodes);
~SNTPrefixTree();
private:
///
/// SantaPrefixNode is a wrapper class that represents one byte.
/// 1 node can represent a whole ASCII character.
/// For example a pointer to the 'A' node will be stored at children[0x41].
/// It takes 1-4 nodes to represent a UTF-8 encoded Unicode character.
///
/// The path for "/🤘" would look like this:
/// children[0x2f] -> children[0xf0] -> children[0x9f] -> children[0xa4]
/// -> children[0x98]
///
/// The path for "/dev" is:
/// children[0x2f] -> children[0x64] -> children[0x65] -> children[0x76]
///
/// Lookups of children are O(1).
///
/// Having the nodes represented by a smaller width, such as a nibble (1/2
/// byte), would drastically decrease the memory footprint but would double
/// required dereferences.
///
/// TODO(bur): Potentially convert this into a full on radix tree.
///
class SantaPrefixNode {
public:
bool isPrefix;
SantaPrefixNode *children[256];
};
// PruneNode will remove the passed in node from the tree.
// The passed in node and all subnodes will be deleted.
// It is the caller's responsibility to reset the pointer to this node (held
// by the parent). If the tree is in use grab the exclusive lock.
void PruneNode(SantaPrefixNode *);
SantaPrefixNode *root_;
// Each node takes up ~2k, assuming MAXPATHLEN is 1024 max out at ~2MB.
static const uint32_t kDefaultMaxNodes = MAXPATHLEN;
uint32_t max_nodes_;
uint32_t node_count_;
pthread_rwlock_t spt_lock_;
std::mutex *spt_add_lock_;
};
#endif /* SANTA__SANTA_DRIVER__SANTAPREFIXTREE_H */

View File

@@ -1,73 +0,0 @@
/// Copyright 2018 Google Inc. All rights reserved.
///
/// 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
///
/// http://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 <XCTest/XCTest.h>
#include "Source/common/SNTPrefixTree.h"
@interface SNTPrefixTreeTest : XCTestCase
@end
@implementation SNTPrefixTreeTest
- (void)testAddAndHas {
auto t = SNTPrefixTree();
XCTAssertFalse(t.HasPrefix("/private/var/tmp/file1"));
t.AddPrefix("/private/var/tmp/");
XCTAssertTrue(t.HasPrefix("/private/var/tmp/file1"));
}
- (void)testReset {
auto t = SNTPrefixTree();
t.AddPrefix("/private/var/tmp/");
XCTAssertTrue(t.HasPrefix("/private/var/tmp/file1"));
t.Reset();
XCTAssertFalse(t.HasPrefix("/private/var/tmp/file1"));
}
- (void)testThreading {
uint32_t count = 4096;
auto t = new SNTPrefixTree(count * (uint32_t)[NSUUID UUID].UUIDString.length);
NSMutableArray *UUIDs = [NSMutableArray arrayWithCapacity:count];
for (int i = 0; i < count; ++i) {
[UUIDs addObject:[NSUUID UUID].UUIDString];
}
__block BOOL stop = NO;
// Create a bunch of background noise.
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (uint64_t i = 0; i < UINT64_MAX; ++i) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
t->HasPrefix([UUIDs[i % count] UTF8String]);
});
if (stop) return;
}
});
// Fill up the tree.
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t i) {
XCTAssertEqual(t->AddPrefix([UUIDs[i] UTF8String]), kIOReturnSuccess);
});
// Make sure every leaf byte is found.
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t i) {
XCTAssertTrue(t->HasPrefix([UUIDs[i] UTF8String]));
});
stop = YES;
}
@end

View File

@@ -33,6 +33,15 @@
_type = type;
_customMsg = customMsg;
_timestamp = timestamp;
if (_type == SNTRuleTypeBinary || _type == SNTRuleTypeCertificate) {
NSCharacterSet *nonHex =
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
if ([[_identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length != 64)
return nil;
} else if (_identifier.length == 0) {
return nil;
}
}
return self;
}
@@ -55,52 +64,52 @@
- (instancetype)initWithDictionary:(NSDictionary *)dict {
if (![dict isKindOfClass:[NSDictionary class]]) return nil;
self = [super init];
if (self) {
_identifier = dict[kRuleIdentifier];
if (![_identifier isKindOfClass:[NSString class]] || !_identifier.length) {
_identifier = dict[kRuleSHA256];
}
if (![_identifier isKindOfClass:[NSString class]] || !_identifier.length) return nil;
NSString *policyString = dict[kRulePolicy];
if (![policyString isKindOfClass:[NSString class]]) return nil;
if ([policyString isEqual:kRulePolicyAllowlist] ||
[policyString isEqual:kRulePolicyAllowlistDeprecated]) {
_state = SNTRuleStateAllow;
} else if ([policyString isEqual:kRulePolicyAllowlistCompiler] ||
[policyString isEqual:kRulePolicyAllowlistCompilerDeprecated]) {
_state = SNTRuleStateAllowCompiler;
} else if ([policyString isEqual:kRulePolicyBlocklist] ||
[policyString isEqual:kRulePolicyBlocklistDeprecated]) {
_state = SNTRuleStateBlock;
} else if ([policyString isEqual:kRulePolicySilentBlocklist] ||
[policyString isEqual:kRulePolicySilentBlocklistDeprecated]) {
_state = SNTRuleStateSilentBlock;
} else if ([policyString isEqual:kRulePolicyRemove]) {
_state = SNTRuleStateRemove;
} else {
return nil;
}
NSString *ruleTypeString = dict[kRuleType];
if (![ruleTypeString isKindOfClass:[NSString class]]) return nil;
if ([ruleTypeString isEqual:kRuleTypeBinary]) {
_type = SNTRuleTypeBinary;
} else if ([ruleTypeString isEqual:kRuleTypeCertificate]) {
_type = SNTRuleTypeCertificate;
} else if ([ruleTypeString isEqual:kRuleTypeTeamID]) {
_type = SNTRuleTypeTeamID;
} else {
return nil;
}
NSString *customMsg = dict[kRuleCustomMsg];
if ([customMsg isKindOfClass:[NSString class]] && customMsg.length) {
_customMsg = customMsg;
}
NSString *identifier = dict[kRuleIdentifier];
if (![identifier isKindOfClass:[NSString class]] || !identifier.length) {
identifier = dict[kRuleSHA256];
}
return self;
if (![identifier isKindOfClass:[NSString class]] || !identifier.length) return nil;
NSString *policyString = dict[kRulePolicy];
SNTRuleState state;
if (![policyString isKindOfClass:[NSString class]]) return nil;
if ([policyString isEqual:kRulePolicyAllowlist] ||
[policyString isEqual:kRulePolicyAllowlistDeprecated]) {
state = SNTRuleStateAllow;
} else if ([policyString isEqual:kRulePolicyAllowlistCompiler] ||
[policyString isEqual:kRulePolicyAllowlistCompilerDeprecated]) {
state = SNTRuleStateAllowCompiler;
} else if ([policyString isEqual:kRulePolicyBlocklist] ||
[policyString isEqual:kRulePolicyBlocklistDeprecated]) {
state = SNTRuleStateBlock;
} else if ([policyString isEqual:kRulePolicySilentBlocklist] ||
[policyString isEqual:kRulePolicySilentBlocklistDeprecated]) {
state = SNTRuleStateSilentBlock;
} else if ([policyString isEqual:kRulePolicyRemove]) {
state = SNTRuleStateRemove;
} else {
return nil;
}
NSString *ruleTypeString = dict[kRuleType];
SNTRuleType type;
if (![ruleTypeString isKindOfClass:[NSString class]]) return nil;
if ([ruleTypeString isEqual:kRuleTypeBinary]) {
type = SNTRuleTypeBinary;
} else if ([ruleTypeString isEqual:kRuleTypeCertificate]) {
type = SNTRuleTypeCertificate;
} else if ([ruleTypeString isEqual:kRuleTypeTeamID]) {
type = SNTRuleTypeTeamID;
} else {
return nil;
}
NSString *customMsg = dict[kRuleCustomMsg];
if (![customMsg isKindOfClass:[NSString class]] || customMsg.length == 0) {
customMsg = nil;
}
return [self initWithIdentifier:identifier state:state type:type customMsg:customMsg];
}
#pragma mark NSSecureCoding

View File

@@ -25,22 +25,24 @@
SNTRule *sut;
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"some-sort-of-identifier",
@"identifier" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"BINARY",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
XCTAssertEqualObjects(sut.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
XCTAssertEqual(sut.type, SNTRuleTypeBinary);
XCTAssertEqual(sut.state, SNTRuleStateAllow);
sut = [[SNTRule alloc] initWithDictionary:@{
@"sha256" : @"some-sort-of-identifier",
@"sha256" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
@"policy" : @"BLOCKLIST",
@"rule_type" : @"CERTIFICATE",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
XCTAssertEqualObjects(sut.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
XCTAssertEqual(sut.type, SNTRuleTypeCertificate);
XCTAssertEqual(sut.state, SNTRuleStateBlock);
@@ -55,12 +57,13 @@
XCTAssertEqual(sut.state, SNTRuleStateSilentBlock);
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"some-sort-of-identifier",
@"identifier" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
@"policy" : @"ALLOWLIST_COMPILER",
@"rule_type" : @"BINARY",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
XCTAssertEqualObjects(sut.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
XCTAssertEqual(sut.type, SNTRuleTypeBinary);
XCTAssertEqual(sut.state, SNTRuleStateAllowCompiler);
@@ -94,12 +97,19 @@
XCTAssertNil(sut);
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"an-identifier",
@"identifier" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
}];
XCTAssertNil(sut);
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"an-identifier",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"BINARY",
}];
XCTAssertNil(sut);
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
@"policy" : @"OTHERPOLICY",
@"rule_type" : @"BINARY",
}];

View File

@@ -14,7 +14,8 @@
#import <Foundation/Foundation.h>
extern NSString *const kXSRFToken;
extern NSString *const kDefaultXSRFTokenHeader;
extern NSString *const kXSRFTokenHeader;
extern NSString *const kSerialNumber;
extern NSString *const kHostname;

View File

@@ -14,7 +14,8 @@
#import "Source/common/SNTSyncConstants.h"
NSString *const kXSRFToken = @"X-XSRF-TOKEN";
NSString *const kDefaultXSRFTokenHeader = @"X-XSRF-TOKEN";
NSString *const kXSRFTokenHeader = @"X-XSRF-TOKEN-HEADER";
NSString *const kSerialNumber = @"serial_num";
NSString *const kHostname = @"hostname";

View File

@@ -15,8 +15,8 @@
#import <Foundation/Foundation.h>
#import <MOLCertificate/MOLCertificate.h>
#import "Source/common/SNTCommon.h"
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SantaVnode.h"
@class SNTRule;
@class SNTStoredEvent;
@@ -31,7 +31,7 @@
/// Cache Ops
///
- (void)cacheCounts:(void (^)(uint64_t rootCache, uint64_t nonRootCache))reply;
- (void)checkCacheForVnodeID:(santa_vnode_id_t)vnodeID withReply:(void (^)(santa_action_t))reply;
- (void)checkCacheForVnodeID:(SantaVnode)vnodeID withReply:(void (^)(SNTAction))reply;
///
/// Database ops
@@ -63,6 +63,7 @@
/// Config ops
///
- (void)watchdogInfo:(void (^)(uint64_t, uint64_t, double, double))reply;
- (void)watchItemsState:(void (^)(BOOL, uint64_t, NSString *, NSString *, NSTimeInterval))reply;
- (void)clientMode:(void (^)(SNTClientMode))reply;
- (void)fullSyncLastSuccess:(void (^)(NSDate *))reply;
- (void)ruleSyncLastSuccess:(void (^)(NSDate *))reply;

View File

@@ -25,7 +25,7 @@
#include <cstdlib>
#include <cstring>
#include "Source/common/SNTCommon.h"
#include "Source/common/BranchPrediction.h"
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"

View File

@@ -0,0 +1,44 @@
/// Copyright 2022 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__SANTAVNODE_H
#define SANTA__COMMON__SANTAVNODE_H
#include <EndpointSecurity/EndpointSecurity.h>
#include <sys/types.h>
// Struct to manage vnode IDs
typedef struct SantaVnode {
dev_t fsid;
ino_t fileid;
#ifdef __cplusplus
bool operator==(const SantaVnode &rhs) const {
return fsid == rhs.fsid && fileid == rhs.fileid;
}
static inline SantaVnode VnodeForFile(const struct stat &sb) {
return SantaVnode{
.fsid = sb.st_dev,
.fileid = sb.st_ino,
};
}
static inline SantaVnode VnodeForFile(const es_file_t *es_file) {
return VnodeForFile(es_file->stat);
}
#endif
} SantaVnode;
#endif

View File

@@ -0,0 +1,24 @@
/// Copyright 2022 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__SANTAVNODEHASH_H
#define SANTA__COMMON__SANTAVNODEHASH_H
#include "Source/common/SantaCache.h"
#include "Source/common/SantaVnode.h"
template <>
uint64_t SantaCacheHasher<SantaVnode>(SantaVnode const &t);
#endif

View File

@@ -0,0 +1,20 @@
/// Copyright 2022 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/common/SantaVnodeHash.h"
template <>
uint64_t SantaCacheHasher<SantaVnode>(SantaVnode const &t) {
return (SantaCacheHasher<uint64_t>(t.fsid) << 1) ^ SantaCacheHasher<uint64_t>(t.fileid);
}

View File

@@ -0,0 +1,44 @@
/// Copyright 2022 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__SYSTEMRESOURCES_H
#define SANTA__COMMON__SYSTEMRESOURCES_H
#import <Foundation/Foundation.h>
#include <mach/mach_time.h>
#include <sys/cdefs.h>
#include <sys/proc_info.h>
#include <optional>
struct SantaTaskInfo {
uint64_t virtual_size;
uint64_t resident_size;
uint64_t total_user_nanos;
uint64_t total_system_nanos;
};
// Convert mach absolute time to nanoseconds
uint64_t MachTimeToNanos(uint64_t mach_time);
// Convert nanoseconds to mach absolute time
uint64_t NanosToMachTime(uint64_t nanos);
// Add some number of nanoseconds to a given mach time and return the new result
uint64_t AddNanosecondsToMachTime(uint64_t ns, uint64_t machTime);
// Get the result of proc_pidinfo with the PROC_PIDTASKINFO flavor
std::optional<SantaTaskInfo> GetTaskInfo();
#endif

View File

@@ -0,0 +1,79 @@
/// Copyright 2022 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/common/SystemResources.h"
#include <dispatch/dispatch.h>
#include <libproc.h>
#include <mach/kern_return.h>
#include <unistd.h>
#include <optional>
#include "Source/common/SNTLogging.h"
static mach_timebase_info_data_t GetTimebase() {
static dispatch_once_t once_token;
static mach_timebase_info_data_t timebase;
dispatch_once(&once_token, ^{
if (mach_timebase_info(&timebase) != KERN_SUCCESS) {
// This shouldn't fail. Assume transitory and exit the program.
// Hopefully fixes itself on restart...
LOGE(@"Failed to get timebase info. Exiting.");
exit(EXIT_FAILURE);
}
});
return timebase;
}
uint64_t MachTimeToNanos(uint64_t mach_time) {
static mach_timebase_info_data_t timebase = GetTimebase();
return mach_time * timebase.numer / timebase.denom;
}
uint64_t NanosToMachTime(uint64_t nanos) {
static mach_timebase_info_data_t timebase = GetTimebase();
return nanos * timebase.denom / timebase.numer;
}
uint64_t AddNanosecondsToMachTime(uint64_t ns, uint64_t machTime) {
// Convert machtime to nanoseconds
uint64_t nanoTime = MachTimeToNanos(machTime);
// Add the nanosecond offset
nanoTime += ns;
// Convert back to machTime
return NanosToMachTime(nanoTime);
}
std::optional<SantaTaskInfo> GetTaskInfo() {
struct proc_taskinfo pti;
if (proc_pidinfo(getpid(), PROC_PIDTASKINFO, 0, &pti, PROC_PIDTASKINFO_SIZE) <
PROC_PIDTASKINFO_SIZE) {
LOGW(@"Unable to get system resource information");
return std::nullopt;
}
return SantaTaskInfo{
.virtual_size = pti.pti_virtual_size,
.resident_size = pti.pti_resident_size,
.total_user_nanos = MachTimeToNanos(pti.pti_total_user),
.total_system_nanos = MachTimeToNanos(pti.pti_total_system),
};
}

View File

@@ -19,6 +19,7 @@
#include <mach/mach_time.h>
#include <time.h>
#include <uuid/uuid.h>
#include "Source/common/SystemResources.h"
audit_token_t MakeAuditToken(pid_t pid, pid_t pidver) {
return audit_token_t{
@@ -86,24 +87,6 @@ es_process_t MakeESProcess(es_file_t *file, audit_token_t tok, audit_token_t par
};
}
static uint64_t AddMillisToMachTime(uint64_t ms, uint64_t machTime) {
static dispatch_once_t onceToken;
static mach_timebase_info_data_t timebase;
dispatch_once(&onceToken, ^{
mach_timebase_info(&timebase);
});
// Convert given machTime to nanoseconds
uint64_t nanoTime = machTime * timebase.numer / timebase.denom;
// Add the ms offset
nanoTime += (ms * NSEC_PER_MSEC);
// Convert back to machTime
return nanoTime * timebase.denom / timebase.numer;
}
uint32_t MaxSupportedESMessageVersionForCurrentOS() {
// Note: ES message v3 was only in betas.
if (@available(macOS 13.0, *)) {
@@ -122,7 +105,7 @@ uint32_t MaxSupportedESMessageVersionForCurrentOS() {
es_message_t MakeESMessage(es_event_type_t et, es_process_t *proc, ActionType action_type,
uint64_t future_deadline_ms) {
es_message_t es_msg = {
.deadline = AddMillisToMachTime(future_deadline_ms, mach_absolute_time()),
.deadline = AddNanosecondsToMachTime(future_deadline_ms * NSEC_PER_MSEC, mach_absolute_time()),
.process = proc,
.action_type =
(action_type == ActionType::Notify) ? ES_ACTION_TYPE_NOTIFY : ES_ACTION_TYPE_AUTH,

24
Source/common/Unit.h Normal file
View File

@@ -0,0 +1,24 @@
/// Copyright 2022 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__UNIT_H
#define SANTA__COMMON__UNIT_H
namespace santa::common {
struct Unit {};
} // namespace santa::common
#endif

View File

@@ -1,8 +1,4 @@
//
// !!! WARNING !!!
// This proto is for demonstration purposes only and will be changing.
// Do not rely on this format.
//
// Important: This schema is currently in BETA
syntax = "proto3";
@@ -235,10 +231,10 @@ message Execution {
optional FileInfo working_directory = 4;
// List of process arguments
repeated string args = 5;
repeated bytes args = 5;
// List of environment variables
repeated string envs = 6;
repeated bytes envs = 6;
// List of file descriptors
repeated FileDescriptor fds = 7;
@@ -464,6 +460,45 @@ message Allowlist {
optional FileInfo target = 2;
}
// Information about access to a watched path
message FileAccess {
// The process that attempted to access the watched path
optional ProcessInfo instigator = 1;
// The path that was accessed
optional FileInfoLight target = 2;
// The version of the policy when the decision was made
optional string policy_version = 3;
// The name of the specific policy that triggered this log
optional string policy_name = 4;
// The event type that attempted to access the watched path
enum AccessType {
ACCESS_TYPE_UNKNOWN = 0;
ACCESS_TYPE_OPEN = 1;
ACCESS_TYPE_RENAME = 2;
ACCESS_TYPE_UNLINK = 3;
ACCESS_TYPE_LINK = 4;
ACCESS_TYPE_CLONE = 5;
ACCESS_TYPE_EXCHANGEDATA = 6;
ACCESS_TYPE_COPYFILE = 7;
ACCESS_TYPE_CREATE = 8;
ACCESS_TYPE_TRUNCATE = 9;
}
optional AccessType access_type = 5;
// Whether the operation was allowed or denied and why
enum PolicyDecision {
POLICY_DECISION_UNKNOWN = 0;
POLICY_DECISION_DENIED = 1;
POLICY_DECISION_DENIED_INVALID_SIGNATURE = 2;
POLICY_DECISION_ALLOWED_AUDIT_ONLY = 3;
}
optional PolicyDecision policy_decision = 6;
}
// A message encapsulating a single event
message SantaMessage {
// Machine ID of the host emitting this log
@@ -489,6 +524,7 @@ message SantaMessage {
Disk disk = 18;
Bundle bundle = 19;
Allowlist allowlist = 20;
FileAccess file_access = 21;
};
}

View File

@@ -1,4 +1,5 @@
load("@build_bazel_rules_apple//apple:macos.bzl", "macos_application")
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load("//:helper.bzl", "santa_unit_test")
licenses(["notice"])
@@ -11,6 +12,25 @@ exports_files([
"Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-256.png",
])
swift_library(
name = "SNTAboutWindowView",
srcs = ["SNTAboutWindowView.swift"],
generates_header = 1,
deps = ["//Source/common:SNTConfigurator"],
)
swift_library(
name = "SNTDeviceMessageWindowView",
srcs = [
"SNTDeviceMessageWindowView.swift",
],
generates_header = 1,
deps = [
"//Source/common:SNTConfigurator",
"//Source/common:SNTDeviceEvent",
],
)
objc_library(
name = "SantaGUI_lib",
srcs = [
@@ -24,8 +44,6 @@ objc_library(
"SNTBinaryMessageWindowController.m",
"SNTDeviceMessageWindowController.h",
"SNTDeviceMessageWindowController.m",
"SNTMessageWindow.h",
"SNTMessageWindow.m",
"SNTMessageWindowController.h",
"SNTMessageWindowController.m",
"SNTNotificationManager.h",
@@ -36,8 +54,6 @@ objc_library(
"SNTNotificationManager.h",
],
data = [
"Resources/AboutWindow.xib",
"Resources/DeviceMessageWindow.xib",
"Resources/MessageWindow.xib",
],
sdk_frameworks = [
@@ -47,6 +63,8 @@ objc_library(
"UserNotifications",
],
deps = [
":SNTAboutWindowView",
":SNTDeviceMessageWindowView",
"//Source/common:SNTBlockMessage_SantaGUI",
"//Source/common:SNTConfigurator",
"//Source/common:SNTDeviceEvent",
@@ -85,7 +103,7 @@ macos_application(
"//conditions:default": None,
}),
infoplists = ["Info.plist"],
minimum_os_version = "10.15",
minimum_os_version = "11.0",
provisioning_profile = select({
"//:adhoc_build": None,
"//conditions:default": "//profiles:santa_dev",

View File

@@ -1,95 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="SNTAboutWindowController">
<connections>
<outlet property="aboutTextField" destination="uh6-q0-RzL" id="oGn-hV-wwc"/>
<outlet property="moreInfoButton" destination="SRu-Kf-vu5" id="Vj2-9Q-05d"/>
<outlet property="window" destination="F0z-JX-Cv5" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Santa" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="F0z-JX-Cv5">
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
<rect key="contentRect" x="196" y="240" width="480" height="200"/>
<rect key="screenRect" x="0.0" y="0.0" width="1728" height="1079"/>
<view key="contentView" id="se5-gp-TjO">
<rect key="frame" x="0.0" y="0.0" width="480" height="200"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="BnL-ZS-kXw">
<rect key="frame" x="199" y="140" width="83" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="UK2-2L-lPx"/>
<constraint firstAttribute="width" constant="79" id="lDf-D7-qlY"/>
</constraints>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Santa" id="VVj-gU-bzy">
<font key="font" size="34" name="HelveticaNeue-UltraLight"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="uh6-q0-RzL">
<rect key="frame" x="18" y="65" width="444" height="60"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" sendsActionOnEndEditing="YES" alignment="center" id="CcT-ul-1eA">
<font key="font" metaFont="system"/>
<string key="title">Santa is an application control system for macOS.
There are no user-configurable settings.</string>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SRu-Kf-vu5">
<rect key="frame" x="129" y="21" width="113" height="32"/>
<constraints>
<constraint firstAttribute="width" constant="99" id="JHv-2J-QSe"/>
</constraints>
<buttonCell key="cell" type="push" title="More Info..." bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="6fe-ju-aET">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="openMoreInfoURL:" target="-2" id="dps-TN-rkS"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Udo-BY-n7e">
<rect key="frame" x="239" y="21" width="113" height="32"/>
<constraints>
<constraint firstAttribute="width" constant="99" id="2Xc-ax-2bV"/>
</constraints>
<buttonCell key="cell" type="push" title="Dismiss" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="uSw-o1-lWW">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
DQ
</string>
</buttonCell>
<connections>
<action selector="orderOut:" target="-1" id="6oW-zI-zn5"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="Udo-BY-n7e" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" priority="900" constant="191" id="1T4-DB-Dz8"/>
<constraint firstItem="SRu-Kf-vu5" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" constant="136" id="Ake-nU-qhW"/>
<constraint firstItem="BnL-ZS-kXw" firstAttribute="top" secondItem="se5-gp-TjO" secondAttribute="top" constant="20" symbolic="YES" id="Fj1-SG-mzF"/>
<constraint firstAttribute="bottom" secondItem="Udo-BY-n7e" secondAttribute="bottom" constant="28" id="bpF-hC-haN"/>
<constraint firstAttribute="bottom" secondItem="SRu-Kf-vu5" secondAttribute="bottom" constant="28" id="fCB-02-SEt"/>
<constraint firstItem="BnL-ZS-kXw" firstAttribute="centerX" secondItem="se5-gp-TjO" secondAttribute="centerX" id="kez-S0-6Gg"/>
<constraint firstItem="Udo-BY-n7e" firstAttribute="leading" secondItem="SRu-Kf-vu5" secondAttribute="trailing" constant="11" id="sYO-yY-w9w"/>
</constraints>
</view>
<connections>
<outlet property="delegate" destination="-2" id="0bl-1N-AYu"/>
</connections>
<point key="canvasLocation" x="323" y="317"/>
</window>
</objects>
</document>

View File

@@ -1,193 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="SNTDeviceMessageWindowController">
<connections>
<outlet property="window" destination="9Bq-yh-54f" id="Uhs-WF-TV9"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Santa Blocked Execution" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" visibleAtLaunch="NO" animationBehavior="none" id="9Bq-yh-54f" customClass="SNTMessageWindow">
<windowStyleMask key="styleMask" utility="YES"/>
<rect key="contentRect" x="167" y="107" width="515" height="318"/>
<rect key="screenRect" x="0.0" y="0.0" width="1728" height="1079"/>
<view key="contentView" id="Iwq-Lx-rLv">
<rect key="frame" x="0.0" y="0.0" width="515" height="318"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<userGuides>
<userLayoutGuide location="344" affinity="minX"/>
</userGuides>
<subviews>
<button focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kiB-jZ-69S">
<rect key="frame" x="16" y="290" width="37" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Hidden Button" alternateTitle="This button exists so neither of the other two buttons is pre-selected when the dialog opens." bezelStyle="rounded" alignment="center" borderStyle="border" focusRingType="none" transparent="YES" imageScaling="proportionallyDown" inset="2" id="XGa-Sl-F4t">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
</userDefinedRuntimeAttributes>
</buttonCell>
</button>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cD5-Su-lXR" customClass="SNTAccessibleTextField">
<rect key="frame" x="31" y="210" width="454" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="450" id="XgJ-EV-tBa"/>
</constraints>
<textFieldCell key="cell" selectable="YES" refusesFirstResponder="YES" allowsUndo="NO" sendsActionOnEndEditing="YES" alignment="center" title="A message to the user goes here..." allowsEditingTextAttributes="YES" id="5tH-bG-UJA">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="value" keyPath="self.attributedCustomMessage" id="376-sj-4Q1"/>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="d9e-Wv-Y5H" userLabel="Label: Path">
<rect key="frame" x="8" y="139" width="142" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="138" id="Kqd-nX-7df"/>
</constraints>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Device Name" id="KgY-X1-ESG">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
</userDefinedRuntimeAttributes>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YNz-ka-cBi" userLabel="Label: Path">
<rect key="frame" x="8" y="115" width="142" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="138" id="3wU-P0-gAC"/>
</constraints>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Device BSD Path" id="adC-be-Beh">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
</userDefinedRuntimeAttributes>
</textFieldCell>
</textField>
<textField toolTip="Device Name" verticalHuggingPriority="900" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" ambiguous="YES" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pc8-G9-4pJ" customClass="SNTAccessibleTextField">
<rect key="frame" x="187" y="139" width="315" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="311" id="xVR-j3-dLw"/>
</constraints>
<textFieldCell key="cell" selectable="YES" sendsActionOnEndEditing="YES" alignment="left" title="Device Name" id="E7T-9h-ofr">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="value" keyPath="self.event.mntonname" id="bOu-gv-1Vh"/>
</connections>
</textField>
<textField toolTip="Device BSD Path" verticalHuggingPriority="900" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" ambiguous="YES" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="bDE-Tl-UHg" customClass="SNTAccessibleTextField">
<rect key="frame" x="187" y="115" width="315" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="311" id="p1W-f9-KBX"/>
</constraints>
<textFieldCell key="cell" selectable="YES" sendsActionOnEndEditing="YES" alignment="left" title="Device BSD Path" id="H1b-Ui-CYo">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="value" keyPath="self.event.mntfromname" id="Sry-KY-HDb"/>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="t8c-Fx-e5h">
<rect key="frame" x="217" y="248" width="82" height="40"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" refusesFirstResponder="YES" sendsActionOnEndEditing="YES" title="Santa" id="7YA-iB-Zma">
<font key="font" metaFont="systemUltraLight" size="34"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
</userDefinedRuntimeAttributes>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="i8e-8n-tZS" userLabel="Label: Path">
<rect key="frame" x="8" y="91" width="142" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="138" id="a3r-ng-zxJ"/>
</constraints>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Remount Mode" id="4D0-8L-ihK">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
</userDefinedRuntimeAttributes>
</textFieldCell>
</textField>
<textField toolTip="Device BSD Path" verticalHuggingPriority="750" ambiguous="YES" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kmz-be-oWf" customClass="SNTAccessibleTextField">
<rect key="frame" x="187" y="91" width="315" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="311" id="cbM-i5-bJ9"/>
</constraints>
<textFieldCell key="cell" selectable="YES" sendsActionOnEndEditing="YES" alignment="left" title="Remount Mode" id="qI0-Na-kxN">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="value" keyPath="self.event.readableRemountArgs" id="bOu-gv-1Vl"/>
</connections>
</textField>
<button verticalHuggingPriority="750" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="BbV-3h-mmL" userLabel="Dismiss Button">
<rect key="frame" x="202" y="18" width="112" height="25"/>
<constraints>
<constraint firstAttribute="width" constant="110" id="6Uh-Bd-N64"/>
<constraint firstAttribute="height" constant="22" id="GH6-nw-6rD"/>
</constraints>
<buttonCell key="cell" type="roundTextured" title="Ok" bezelStyle="texturedRounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="XR6-Xa-gP4">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
DQ
</string>
<modifierMask key="keyEquivalentModifierMask" shift="YES"/>
</buttonCell>
<accessibility description="Dismiss Dialog"/>
<connections>
<action selector="closeWindow:" target="-2" id="qQq-gh-8lw"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="BbV-3h-mmL" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" priority="500" constant="193" id="2uo-Cm-Tfp"/>
<constraint firstItem="kmz-be-oWf" firstAttribute="height" secondItem="i8e-8n-tZS" secondAttribute="height" id="6Ow-Sl-6Wc"/>
<constraint firstItem="d9e-Wv-Y5H" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" constant="10" id="FPe-Rd-G4n"/>
<constraint firstItem="t8c-Fx-e5h" firstAttribute="top" secondItem="Iwq-Lx-rLv" secondAttribute="top" constant="30" id="FuB-GX-0jg"/>
<constraint firstItem="YNz-ka-cBi" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" constant="10" id="KmX-kX-VCN"/>
<constraint firstItem="pc8-G9-4pJ" firstAttribute="top" secondItem="cD5-Su-lXR" secondAttribute="bottom" priority="950" constant="55" id="Nsl-zf-poH"/>
<constraint firstItem="YNz-ka-cBi" firstAttribute="centerY" secondItem="bDE-Tl-UHg" secondAttribute="centerY" id="ObQ-RA-S5V"/>
<constraint firstItem="d9e-Wv-Y5H" firstAttribute="centerY" secondItem="pc8-G9-4pJ" secondAttribute="centerY" id="SLv-F7-w5k"/>
<constraint firstAttribute="centerX" secondItem="cD5-Su-lXR" secondAttribute="centerX" id="V0a-Py-iEc"/>
<constraint firstItem="cD5-Su-lXR" firstAttribute="top" secondItem="t8c-Fx-e5h" secondAttribute="bottom" constant="22" id="dYg-zP-wh2"/>
<constraint firstAttribute="centerX" secondItem="t8c-Fx-e5h" secondAttribute="centerX" id="h3d-Kc-q88"/>
<constraint firstItem="kmz-be-oWf" firstAttribute="centerY" secondItem="i8e-8n-tZS" secondAttribute="centerY" id="ilC-5x-LNe"/>
<constraint firstItem="bDE-Tl-UHg" firstAttribute="top" secondItem="pc8-G9-4pJ" secondAttribute="bottom" constant="8" id="pis-of-f93"/>
<constraint firstItem="i8e-8n-tZS" firstAttribute="centerY" secondItem="YNz-ka-cBi" secondAttribute="centerY" constant="24" id="uZs-XT-PuZ"/>
<constraint firstAttribute="bottom" secondItem="BbV-3h-mmL" secondAttribute="bottom" constant="35" id="ukF-FH-DE8"/>
<constraint firstItem="YNz-ka-cBi" firstAttribute="firstBaseline" secondItem="d9e-Wv-Y5H" secondAttribute="baseline" constant="24" id="xZu-AY-wX5"/>
<constraint firstItem="i8e-8n-tZS" firstAttribute="top" secondItem="YNz-ka-cBi" secondAttribute="bottom" constant="8" symbolic="YES" id="ylw-3s-9JW"/>
</constraints>
</view>
<connections>
<outlet property="initialFirstResponder" destination="kiB-jZ-69S" id="I96-dS-lq5"/>
</connections>
<point key="canvasLocation" x="261.5" y="246"/>
</window>
<userDefaultsController representsSharedInstance="YES" id="iXx-cu-WYe"/>
</objects>
</document>

View File

@@ -14,11 +14,5 @@
#import <Cocoa/Cocoa.h>
@interface SNTAboutWindowController : NSWindowController
@property IBOutlet NSTextField *aboutTextField;
@property IBOutlet NSButton *moreInfoButton;
- (IBAction)openMoreInfoURL:(id)sender;
@interface SNTAboutWindowController : NSWindowController <NSWindowDelegate>
@end

View File

@@ -13,30 +13,35 @@
/// limitations under the License.
#import "Source/gui/SNTAboutWindowController.h"
#import "Source/gui/SNTAboutWindowView-Swift.h"
#import "Source/common/SNTConfigurator.h"
@implementation SNTAboutWindowController
- (instancetype)init {
return [super initWithWindowNibName:@"AboutWindow"];
- (void)showWindow:(id)sender {
[super showWindow:sender];
if (self.window) [self.window orderOut:sender];
self.window =
[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 0, 0)
styleMask:NSWindowStyleMaskClosable | NSWindowStyleMaskTitled
backing:NSBackingStoreBuffered
defer:NO];
self.window.contentViewController = [SNTAboutWindowViewFactory createWithWindow:self.window];
self.window.title = @"Santa";
self.window.delegate = self;
[self.window makeKeyAndOrderFront:nil];
[self.window center];
// Add app to Cmd+Tab and Dock.
NSApp.activationPolicy = NSApplicationActivationPolicyRegular;
}
- (void)loadWindow {
[super loadWindow];
SNTConfigurator *config = [SNTConfigurator configurator];
NSString *aboutText = [config aboutText];
if (aboutText) {
[self.aboutTextField setStringValue:aboutText];
}
if (![config moreInfoURL]) {
[self.moreInfoButton removeFromSuperview];
}
}
- (IBAction)openMoreInfoURL:(id)sender {
[[NSWorkspace sharedWorkspace] openURL:[[SNTConfigurator configurator] moreInfoURL]];
[self close];
- (void)windowWillClose:(NSNotification *)notification {
// Remove app from Cmd+Tab and Dock.
NSApp.activationPolicy = NSApplicationActivationPolicyAccessory;
}
@end

View File

@@ -0,0 +1,63 @@
import SwiftUI
import santa_common_SNTConfigurator
@objc public class SNTAboutWindowViewFactory : NSObject {
@objc public static func createWith(window: NSWindow) -> NSViewController {
return NSHostingController(rootView:SNTAboutWindowView(w:window).frame(width:400, height:200))
}
}
struct SNTAboutWindowView: View {
let w: NSWindow?
let c = SNTConfigurator()
var body: some View {
VStack(spacing:20.0) {
Text("Santa").font(Font.custom("HelveticaNeue-UltraLight", size: 34.0))
if let t = c.aboutText {
Text(t).multilineTextAlignment(.center)
} else {
Text("""
Santa is an application control system for macOS.
There are no user-configurable settings.
""").multilineTextAlignment(.center)
}
HStack {
if c.moreInfoURL?.absoluteString.isEmpty == false {
Button(action: moreInfoButton) {
Text("More Info...").frame(width: 90.0)
}
}
Button(action: dismissButton) {
Text("Dismiss").frame(width: 90.0)
}
.keyboardShortcut(.defaultAction)
}.padding(10.0)
}
}
func dismissButton() {
w?.close()
}
func moreInfoButton() {
if let u = c.moreInfoURL {
NSWorkspace.shared.open(u)
}
w?.close()
}
}
// Enable previews in Xcode.
struct SNTAboutWindow_Previews: PreviewProvider {
static var previews: some View {
SNTAboutWindowView(w: nil)
}
}

View File

@@ -58,7 +58,9 @@
}
- (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag {
self.aboutWindowController = [[SNTAboutWindowController alloc] init];
if (!self.aboutWindowController) {
self.aboutWindowController = [[SNTAboutWindowController alloc] init];
}
[self.aboutWindowController showWindow:self];
return NO;
}

View File

@@ -20,7 +20,6 @@
#import "Source/common/SNTBlockMessage.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTStoredEvent.h"
#import "Source/gui/SNTMessageWindow.h"
@interface SNTBinaryMessageWindowController ()
/// The custom message to display for this event

View File

@@ -23,10 +23,7 @@ NS_ASSUME_NONNULL_BEGIN
///
/// Controller for a single message window.
///
@interface SNTDeviceMessageWindowController : SNTMessageWindowController
@property(weak) IBOutlet NSTextField *remountArgsLabel;
@property(weak) IBOutlet NSTextField *remountArgsTitle;
@interface SNTDeviceMessageWindowController : SNTMessageWindowController <NSWindowDelegate>
// The device event this window is for.
@property(readonly) SNTDeviceEvent *event;

View File

@@ -13,11 +13,11 @@
/// limitations under the License.
#import "Source/gui/SNTDeviceMessageWindowController.h"
#import "Source/gui/SNTDeviceMessageWindowView-Swift.h"
#import "Source/common/SNTBlockMessage.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTDeviceEvent.h"
#import "Source/gui/SNTMessageWindow.h"
NS_ASSUME_NONNULL_BEGIN
@@ -28,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN
@implementation SNTDeviceMessageWindowController
- (instancetype)initWithEvent:(SNTDeviceEvent *)event message:(nullable NSString *)message {
self = [super initWithWindowNibName:@"DeviceMessageWindow"];
self = [super init];
if (self) {
_event = event;
_customMessage = message;
@@ -36,12 +36,30 @@ NS_ASSUME_NONNULL_BEGIN
return self;
}
- (void)loadWindow {
[super loadWindow];
if (!self.event.remountArgs || [self.event.remountArgs count] <= 0) {
[self.remountArgsLabel removeFromSuperview];
[self.remountArgsTitle removeFromSuperview];
}
- (void)showWindow:(id)sender {
if (self.window) [self.window orderOut:sender];
self.window =
[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 0, 0)
styleMask:NSWindowStyleMaskClosable | NSWindowStyleMaskTitled
backing:NSBackingStoreBuffered
defer:NO];
self.window.contentViewController =
[SNTDeviceMessageWindowViewFactory createWithWindow:self.window
event:self.event
customMsg:self.attributedCustomMessage];
self.window.delegate = self;
// Add app to Cmd+Tab and Dock.
NSApp.activationPolicy = NSApplicationActivationPolicyRegular;
[super showWindow:sender];
}
- (void)windowWillClose:(NSNotification *)notification {
// Remove app from Cmd+Tab and Dock.
NSApp.activationPolicy = NSApplicationActivationPolicyAccessory;
[super windowWillClose:notification];
}
- (NSAttributedString *)attributedCustomMessage {

View File

@@ -0,0 +1,76 @@
import SwiftUI
import santa_common_SNTConfigurator
import santa_common_SNTDeviceEvent
@objc public class SNTDeviceMessageWindowViewFactory : NSObject {
@objc public static func createWith(window: NSWindow, event: SNTDeviceEvent, customMsg: NSAttributedString?) -> NSViewController {
return NSHostingController(rootView:SNTDeviceMessageWindowView(window:window, event:event, customMsg:customMsg).frame(width:450, height:300))
}
}
struct SNTDeviceMessageWindowView: View {
let window: NSWindow?
let event: SNTDeviceEvent?
let customMsg: NSAttributedString?
let c = SNTConfigurator()
var body: some View {
VStack(spacing:20.0) {
Text("Santa").font(Font.custom("HelveticaNeue-UltraLight", size: 34.0))
if let t = customMsg {
if #available(macOS 12.0, *) {
let a = AttributedString(t)
Text(a).multilineTextAlignment(.center).padding(15.0)
} else {
Text(t.description).multilineTextAlignment(.center).padding(15.0)
}
} else {
Text("Mounting devices is blocked")
}
HStack(spacing:5.0) {
VStack(alignment: .trailing, spacing: 8.0) {
Text("Device Name").bold()
Text("Device BSD Path").bold()
if event!.remountArgs.count > 0 {
Text("Remount Mode").bold()
}
}
Spacer().frame(width: 10.0)
VStack(alignment: .leading, spacing: 8.0) {
Text(event!.mntonname)
Text(event!.mntfromname)
if event!.remountArgs.count > 0 {
Text(event!.readableRemountArgs())
}
}
}
HStack {
Button(action: dismissButton) {
Text("OK").frame(width: 90.0)
}
.keyboardShortcut(.defaultAction)
}.padding(10.0)
}
}
func dismissButton() {
window?.close()
}
}
// Enable previews in Xcode.
struct SNTDeviceMessageWindowView_Previews: PreviewProvider {
static var previews: some View {
SNTDeviceMessageWindowView(window: nil, event: nil, customMsg: nil)
}
}

View File

@@ -4,7 +4,7 @@
- (void)windowDidCloseSilenceHash:(NSString *)hash;
@end
@interface SNTMessageWindowController : NSWindowController
@interface SNTMessageWindowController : NSWindowController <NSWindowDelegate>
- (IBAction)showWindow:(id)sender;
- (IBAction)closeWindow:(id)sender;

View File

@@ -1,14 +1,17 @@
#import "Source/gui/SNTMessageWindowController.h"
#import "Source/gui/SNTMessageWindow.h"
@implementation SNTMessageWindowController
- (IBAction)showWindow:(id)sender {
[(SNTMessageWindow *)self.window fadeIn:sender];
[self.window setLevel:NSPopUpMenuWindowLevel];
[self.window setMovableByWindowBackground:YES];
[self.window makeKeyAndOrderFront:sender];
[self.window center];
[NSApp activateIgnoringOtherApps:YES];
}
- (IBAction)closeWindow:(id)sender {
[(SNTMessageWindow *)self.window fadeOut:sender];
[self windowWillClose:sender];
[self.window close];
}
- (void)windowWillClose:(NSNotification *)notification {
@@ -21,12 +24,6 @@
}
}
- (void)loadWindow {
[super loadWindow];
[self.window setLevel:NSPopUpMenuWindowLevel];
[self.window setMovableByWindowBackground:YES];
}
- (NSString *)messageHash {
[self doesNotRecognizeSelector:_cmd];
return nil;

View File

@@ -92,10 +92,16 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
}
- (void)queueMessage:(SNTMessageWindowController *)pendingMsg {
NSString *messageHash = [pendingMsg messageHash];
// Post a distributed notification, regardless of queue state.
[self postDistributedNotification:pendingMsg];
// If GUI is in silent mode or if there's already a notification queued for
// this message, don't do anything else.
if ([SNTConfigurator configurator].enableSilentMode) return;
if ([self notificationAlreadyQueued:pendingMsg]) return;
// See if this message is silenced.
// See if this message has been user-silenced.
NSString *messageHash = [pendingMsg messageHash];
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSDate *silenceDate = [ud objectForKey:silencedNotificationsKey][messageHash];
if ([silenceDate isKindOfClass:[NSDate class]]) {
@@ -114,7 +120,6 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
pendingMsg.delegate = self;
[self.pendingNotifications addObject:pendingMsg];
[self postDistributedNotification:pendingMsg];
if (!self.currentWindowController) {
[self showQueuedWindow];
@@ -315,8 +320,6 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
}
- (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message {
if ([SNTConfigurator configurator].enableSilentMode) return;
if (!event) {
LOGI(@"Error: Missing event object in message received from daemon!");
return;
@@ -329,8 +332,6 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
}
- (void)postUSBBlockNotification:(SNTDeviceEvent *)event withCustomMessage:(NSString *)message {
if ([SNTConfigurator configurator].enableSilentMode) return;
if (!event) {
LOGI(@"Error: Missing event object in message received from daemon!");
return;

View File

@@ -62,28 +62,24 @@ int main(int argc, const char *argv[]) {
sysxOperation = @(2);
}
if (sysxOperation) {
if (@available(macOS 10.15, *)) {
NSString *e = [SNTXPCControlInterface systemExtensionID];
dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
OSSystemExtensionRequest *req;
if (sysxOperation.intValue == 1) {
NSLog(@"Requesting SystemExtension activation");
req = [OSSystemExtensionRequest activationRequestForExtension:e queue:q];
} else if (sysxOperation.intValue == 2) {
NSLog(@"Requesting SystemExtension deactivation");
req = [OSSystemExtensionRequest deactivationRequestForExtension:e queue:q];
}
if (req) {
SNTSystemExtensionDelegate *ed = [[SNTSystemExtensionDelegate alloc] init];
req.delegate = ed;
[[OSSystemExtensionManager sharedManager] submitRequest:req];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 60), q, ^{
exit(1);
});
[[NSRunLoop mainRunLoop] run];
}
} else {
exit(1);
NSString *e = [SNTXPCControlInterface systemExtensionID];
dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
OSSystemExtensionRequest *req;
if (sysxOperation.intValue == 1) {
NSLog(@"Requesting SystemExtension activation");
req = [OSSystemExtensionRequest activationRequestForExtension:e queue:q];
} else if (sysxOperation.intValue == 2) {
NSLog(@"Requesting SystemExtension deactivation");
req = [OSSystemExtensionRequest deactivationRequestForExtension:e queue:q];
}
if (req) {
SNTSystemExtensionDelegate *ed = [[SNTSystemExtensionDelegate alloc] init];
req.delegate = ed;
[[OSSystemExtensionManager sharedManager] submitRequest:req];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 60), q, ^{
exit(1);
});
[[NSRunLoop mainRunLoop] run];
}
}

View File

@@ -34,7 +34,7 @@ macos_command_line_application(
"--options library,kill,runtime",
],
infoplists = ["Info.plist"],
minimum_os_version = "10.15",
minimum_os_version = "11.0",
provisioning_profile = select({
"//:adhoc_build": None,
"//conditions:default": "//profiles:santa_dev",

View File

@@ -59,7 +59,6 @@ objc_library(
":SNTCommandPrintLog",
":santactl_cmd",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommon",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTDropRootPrivs",
@@ -91,7 +90,7 @@ macos_command_line_application(
"--options library,kill,runtime",
],
infoplists = ["Info.plist"],
minimum_os_version = "10.15",
minimum_os_version = "11.0",
provisioning_profile = select({
"//:adhoc_build": None,
"//conditions:default": "//profiles:santa_dev",

View File

@@ -40,39 +40,39 @@ REGISTER_COMMAND_NAME(@"checkcache")
}
+ (NSString *)shortHelpText {
return @"Prints the status of a file in the kernel cache.";
return @"Prints the status of a file in the cache.";
}
+ (NSString *)longHelpText {
return (@"Checks the in-kernel cache for desired file.\n"
return (@"Checks the cache for desired file.\n"
@"Returns 0 if successful, 1 otherwise");
}
- (void)runWithArguments:(NSArray *)arguments {
santa_vnode_id_t vnodeID = [self vnodeIDForFile:arguments.firstObject];
[[self.daemonConn remoteObjectProxy]
SantaVnode vnodeID = [self vnodeIDForFile:arguments.firstObject];
[[self.daemonConn synchronousRemoteObjectProxy]
checkCacheForVnodeID:vnodeID
withReply:^(santa_action_t action) {
if (action == ACTION_RESPOND_ALLOW) {
LOGI(@"File exists in [allowlist] kernel cache");
withReply:^(SNTAction action) {
if (action == SNTActionRespondAllow) {
LOGI(@"File exists in [allowlist] cache");
exit(0);
} else if (action == ACTION_RESPOND_DENY) {
LOGI(@"File exists in [blocklist] kernel cache");
} else if (action == SNTActionRespondDeny) {
LOGI(@"File exists in [blocklist] cache");
exit(0);
} else if (action == ACTION_RESPOND_ALLOW_COMPILER) {
LOGI(@"File exists in [allowlist compiler] kernel cache");
} else if (action == SNTActionRespondAllowCompiler) {
LOGI(@"File exists in [allowlist compiler] cache");
exit(0);
} else if (action == ACTION_UNSET) {
} else if (action == SNTActionUnset) {
LOGE(@"File does not exist in cache");
exit(1);
}
}];
}
- (santa_vnode_id_t)vnodeIDForFile:(NSString *)path {
- (SantaVnode)vnodeIDForFile:(NSString *)path {
struct stat fstat = {};
stat(path.fileSystemRepresentation, &fstat);
santa_vnode_id_t ret = {.fsid = fstat.st_dev, .fileid = fstat.st_ino};
SantaVnode ret = {.fsid = fstat.st_dev, .fileid = fstat.st_ino};
return ret;
}

View File

@@ -166,19 +166,10 @@ REGISTER_COMMAND_NAME(@"metrics")
- (void)runWithArguments:(NSArray *)arguments {
__block NSDictionary *metrics;
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
[[self.daemonConn remoteObjectProxy] metrics:^(NSDictionary *exportedMetrics) {
[[self.daemonConn synchronousRemoteObjectProxy] metrics:^(NSDictionary *exportedMetrics) {
metrics = exportedMetrics;
dispatch_group_leave(group);
}];
// Wait a maximum of 5s for metrics collected from daemon to arrive.
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5))) {
fprintf(stderr, "Failed to retrieve metrics from daemon\n\n");
}
metrics = [self filterMetrics:metrics withArguments:arguments];
[self prettyPrintMetrics:metrics asJSON:[arguments containsObject:@"--json"]];

View File

@@ -131,9 +131,6 @@ REGISTER_COMMAND_NAME(@"rule")
[self printErrorUsageAndExit:@"--sha256 requires an argument"];
}
newRule.identifier = arguments[i];
if (newRule.identifier.length != 64) {
[self printErrorUsageAndExit:@"--sha256 requires a valid SHA-256 as the argument"];
}
} else if ([arg caseInsensitiveCompare:@"--message"] == NSOrderedSame) {
if (++i > arguments.count - 1) {
[self printErrorUsageAndExit:@"--message requires an argument"];
@@ -148,6 +145,15 @@ REGISTER_COMMAND_NAME(@"rule")
}
}
if (newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate) {
NSCharacterSet *nonHex =
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
if ([[newRule.identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length !=
64) {
[self printErrorUsageAndExit:@"BINARY or CERTIFICATE rules require a valid SHA-256"];
}
}
if (check) {
if (!newRule.identifier) return [self printErrorUsageAndExit:@"--check requires --identifier"];
return [self printStateOfRule:newRule daemonConnection:self.daemonConn];
@@ -210,87 +216,65 @@ REGISTER_COMMAND_NAME(@"rule")
}
- (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;
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
__block NSMutableString *output;
[[daemonConn remoteObjectProxy] decisionForFilePath:nil
fileSHA256:fileSHA256
certificateSHA256:certificateSHA256
teamID:teamID
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;
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"];
}
}
dispatch_group_leave(group);
}];
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
printf("Cannot communicate with daemon");
exit(1);
}
[rop decisionForFilePath:nil
fileSHA256:fileSHA256
certificateSHA256:certificateSHA256
teamID:teamID
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;
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"];
}
}
}];
dispatch_group_enter(group);
[[daemonConn remoteObjectProxy]
databaseRuleForBinarySHA256:fileSHA256
certificateSHA256:certificateSHA256
teamID:teamID
reply:^(SNTRule *r) {
if (r.state == SNTRuleStateAllowTransitive) {
NSDate *date =
[NSDate dateWithTimeIntervalSinceReferenceDate:r.timestamp];
[output
appendString:[NSString stringWithFormat:@"\nlast access date: %@",
[date description]]];
}
dispatch_group_leave(group);
}];
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
printf("Cannot communicate with daemon");
exit(1);
}
[rop databaseRuleForBinarySHA256:fileSHA256
certificateSHA256:certificateSHA256
teamID:teamID
reply:^(SNTRule *r) {
if (r.state == SNTRuleStateAllowTransitive) {
NSDate *date =
[NSDate dateWithTimeIntervalSinceReferenceDate:r.timestamp];
[output appendString:[NSString
stringWithFormat:@"\nlast access date: %@",
[date description]]];
}
}];
printf("%s\n", output.UTF8String);
exit(0);

View File

@@ -46,116 +46,110 @@ REGISTER_COMMAND_NAME(@"status")
- (void)runWithArguments:(NSArray *)arguments {
dispatch_group_t group = dispatch_group_create();
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
// Daemon status
__block NSString *clientMode;
__block uint64_t cpuEvents, ramEvents;
__block double cpuPeak, ramPeak;
dispatch_group_enter(group);
[[self.daemonConn remoteObjectProxy] clientMode:^(SNTClientMode cm) {
[rop clientMode:^(SNTClientMode cm) {
switch (cm) {
case SNTClientModeMonitor: clientMode = @"Monitor"; break;
case SNTClientModeLockdown: clientMode = @"Lockdown"; break;
default: clientMode = [NSString stringWithFormat:@"Unknown (%ld)", cm]; break;
}
dispatch_group_leave(group);
}];
dispatch_group_enter(group);
[[self.daemonConn remoteObjectProxy] watchdogInfo:^(uint64_t wd_cpuEvents, uint64_t wd_ramEvents,
double wd_cpuPeak, double wd_ramPeak) {
[rop watchdogInfo:^(uint64_t wd_cpuEvents, uint64_t wd_ramEvents, double wd_cpuPeak,
double wd_ramPeak) {
cpuEvents = wd_cpuEvents;
cpuPeak = wd_cpuPeak;
ramEvents = wd_ramEvents;
ramPeak = wd_ramPeak;
dispatch_group_leave(group);
}];
BOOL fileLogging = ([[SNTConfigurator configurator] fileChangesRegex] != nil);
NSString *eventLogType = [[[SNTConfigurator configurator] eventLogTypeRaw] lowercaseString];
SNTConfigurator *configurator = [SNTConfigurator configurator];
// Cache status
__block uint64_t rootCacheCount = -1, nonRootCacheCount = -1;
dispatch_group_enter(group);
[[self.daemonConn remoteObjectProxy] cacheCounts:^(uint64_t rootCache, uint64_t nonRootCache) {
[rop cacheCounts:^(uint64_t rootCache, uint64_t nonRootCache) {
rootCacheCount = rootCache;
nonRootCacheCount = nonRootCache;
dispatch_group_leave(group);
}];
// Database counts
__block int64_t eventCount = -1, binaryRuleCount = -1, certRuleCount = -1, teamIDRuleCount = -1;
__block int64_t compilerRuleCount = -1, transitiveRuleCount = -1;
dispatch_group_enter(group);
[[self.daemonConn remoteObjectProxy]
databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler, int64_t transitive,
int64_t teamID) {
binaryRuleCount = binary;
certRuleCount = certificate;
teamIDRuleCount = teamID;
compilerRuleCount = compiler;
transitiveRuleCount = transitive;
dispatch_group_leave(group);
}];
dispatch_group_enter(group);
[[self.daemonConn remoteObjectProxy] databaseEventCount:^(int64_t count) {
[rop databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler,
int64_t transitive, int64_t teamID) {
binaryRuleCount = binary;
certRuleCount = certificate;
teamIDRuleCount = teamID;
compilerRuleCount = compiler;
transitiveRuleCount = transitive;
}];
[rop databaseEventCount:^(int64_t count) {
eventCount = count;
dispatch_group_leave(group);
}];
// Static rule count
__block int64_t staticRuleCount = -1;
dispatch_group_enter(group);
[[self.daemonConn remoteObjectProxy] staticRuleCount:^(int64_t count) {
[rop staticRuleCount:^(int64_t count) {
staticRuleCount = count;
dispatch_group_leave(group);
}];
// Sync status
__block NSDate *fullSyncLastSuccess;
dispatch_group_enter(group);
[[self.daemonConn remoteObjectProxy] fullSyncLastSuccess:^(NSDate *date) {
[rop fullSyncLastSuccess:^(NSDate *date) {
fullSyncLastSuccess = date;
dispatch_group_leave(group);
}];
__block NSDate *ruleSyncLastSuccess;
dispatch_group_enter(group);
[[self.daemonConn remoteObjectProxy] ruleSyncLastSuccess:^(NSDate *date) {
[rop ruleSyncLastSuccess:^(NSDate *date) {
ruleSyncLastSuccess = date;
dispatch_group_leave(group);
}];
__block BOOL syncCleanReqd = NO;
dispatch_group_enter(group);
[[self.daemonConn remoteObjectProxy] syncCleanRequired:^(BOOL clean) {
[rop syncCleanRequired:^(BOOL clean) {
syncCleanReqd = clean;
dispatch_group_leave(group);
}];
__block BOOL pushNotifications = NO;
if ([[SNTConfigurator configurator] syncBaseURL]) {
dispatch_group_enter(group);
[[self.daemonConn remoteObjectProxy] pushNotifications:^(BOOL response) {
[rop pushNotifications:^(BOOL response) {
pushNotifications = response;
dispatch_group_leave(group);
}];
}
__block BOOL enableBundles = NO;
if ([[SNTConfigurator configurator] syncBaseURL]) {
dispatch_group_enter(group);
[[self.daemonConn remoteObjectProxy] enableBundles:^(BOOL response) {
[rop enableBundles:^(BOOL response) {
enableBundles = response;
dispatch_group_leave(group);
}];
}
__block BOOL enableTransitiveRules = NO;
dispatch_group_enter(group);
[[self.daemonConn remoteObjectProxy] enableTransitiveRules:^(BOOL response) {
[rop enableTransitiveRules:^(BOOL response) {
enableTransitiveRules = response;
dispatch_group_leave(group);
}];
__block BOOL watchItemsEnabled = NO;
__block uint64_t watchItemsRuleCount = 0;
__block NSString *watchItemsPolicyVersion = nil;
__block NSString *watchItemsConfigPath = nil;
__block NSTimeInterval watchItemsLastUpdateEpoch = 0;
[rop watchItemsState:^(BOOL enabled, uint64_t ruleCount, NSString *policyVersion,
NSString *configPath, NSTimeInterval lastUpdateEpoch) {
watchItemsEnabled = enabled;
if (enabled) {
watchItemsRuleCount = ruleCount;
watchItemsPolicyVersion = policyVersion;
watchItemsConfigPath = configPath;
watchItemsLastUpdateEpoch = lastUpdateEpoch;
}
}];
// Wait a maximum of 5s for stats collected from daemon to arrive.
@@ -170,6 +164,10 @@ REGISTER_COMMAND_NAME(@"status")
NSString *ruleSyncLastSuccessStr =
[dateFormatter stringFromDate:ruleSyncLastSuccess] ?: fullSyncLastSuccessStr;
NSString *watchItemsLastUpdateStr =
[dateFormatter stringFromDate:[NSDate dateWithTimeIntervalSince1970:watchItemsLastUpdateEpoch]]
?: @"Never";
NSString *syncURLStr = configurator.syncBaseURL.absoluteString;
BOOL exportMetrics = configurator.exportMetrics;
@@ -181,6 +179,7 @@ REGISTER_COMMAND_NAME(@"status")
@"daemon" : @{
@"driver_connected" : @(YES),
@"mode" : clientMode ?: @"null",
@"log_type" : eventLogType,
@"file_logging" : @(fileLogging),
@"watchdog_cpu_events" : @(cpuEvents),
@"watchdog_ram_events" : @(ramEvents),
@@ -212,6 +211,22 @@ REGISTER_COMMAND_NAME(@"status")
},
} mutableCopy];
NSDictionary *watchItems;
if (watchItemsEnabled) {
watchItems = @{
@"enabled" : @(watchItemsEnabled),
@"rule_count" : @(watchItemsRuleCount),
@"policy_version" : watchItemsPolicyVersion,
@"config_path" : watchItemsConfigPath ?: @"null",
@"last_policy_update" : watchItemsLastUpdateStr ?: @"null",
};
} else {
watchItems = @{
@"enabled" : @(watchItemsEnabled),
};
}
stats[@"watch_items"] = watchItems;
stats[@"cache"] = @{
@"root_cache_count" : @(rootCacheCount),
@"non_root_cache_count" : @(nonRootCacheCount),
@@ -225,6 +240,7 @@ REGISTER_COMMAND_NAME(@"status")
} else {
printf(">>> Daemon Info\n");
printf(" %-25s | %s\n", "Mode", [clientMode UTF8String]);
printf(" %-25s | %s\n", "Log Type", [eventLogType UTF8String]);
printf(" %-25s | %s\n", "File Logging", (fileLogging ? "Yes" : "No"));
printf(" %-25s | %s\n", "USB Blocking", (configurator.blockUSBMount ? "Yes" : "No"));
if (configurator.blockUSBMount && configurator.remountUSBMode.count > 0) {
@@ -251,6 +267,15 @@ REGISTER_COMMAND_NAME(@"status")
printf(" %-25s | %lld\n", "Rules", staticRuleCount);
}
printf(">>> Watch Items\n");
printf(" %-25s | %s\n", "Enabled", (watchItemsEnabled ? "Yes" : "No"));
if (watchItemsEnabled) {
printf(" %-25s | %s\n", "Policy Version", watchItemsPolicyVersion.UTF8String);
printf(" %-25s | %llu\n", "Rule Count", watchItemsRuleCount);
printf(" %-25s | %s\n", "Config Path", (watchItemsConfigPath ?: @"(embedded)").UTF8String);
printf(" %-25s | %s\n", "Last Policy Update", watchItemsLastUpdateStr.UTF8String);
}
if (syncURLStr) {
printf(">>> Sync Info\n");
printf(" %-25s | %s\n", "Sync Server", [syncURLStr UTF8String]);

View File

@@ -15,7 +15,6 @@
#import <Foundation/Foundation.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
#import "Source/common/SNTCommon.h"
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTFileInfo.h"

View File

@@ -23,6 +23,7 @@ objc_library(
hdrs = ["DataLayer/SNTRuleTable.h"],
deps = [
":SNTDatabaseTable",
"//Source/common:Platform",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
@@ -34,6 +35,37 @@ objc_library(
],
)
objc_library(
name = "WatchItemPolicy",
hdrs = ["DataLayer/WatchItemPolicy.h"],
)
objc_library(
name = "WatchItems",
srcs = ["DataLayer/WatchItems.mm"],
hdrs = ["DataLayer/WatchItems.h"],
deps = [
":SNTEndpointSecurityEventHandler",
":WatchItemPolicy",
"//Source/common:PrefixTree",
"//Source/common:SNTLogging",
"//Source/common:Unit",
],
)
santa_unit_test(
name = "WatchItemsTest",
srcs = ["DataLayer/WatchItemsTest.mm"],
deps = [
":WatchItemPolicy",
":WatchItems",
"//Source/common:PrefixTree",
"//Source/common:TestUtils",
"//Source/common:Unit",
"@OCMock",
],
)
objc_library(
name = "SNTEventTable",
srcs = ["DataLayer/SNTEventTable.m"],
@@ -63,19 +95,20 @@ objc_library(
deps = [
":EndpointSecurityMessage",
":Metrics",
"//Source/common:SNTCommon",
":WatchItemPolicy",
],
)
objc_library(
name = "SNTApplicationCoreMetrics",
srcs = ["SNTApplicationCoreMetrics.m"],
srcs = ["SNTApplicationCoreMetrics.mm"],
hdrs = ["SNTApplicationCoreMetrics.h"],
deps = [
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTMetricSet",
"//Source/common:SNTSystemInfo",
"//Source/common:SystemResources",
],
)
@@ -104,6 +137,9 @@ objc_library(
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTRule",
"//Source/common:SantaCache",
"//Source/common:SantaVnode",
"//Source/common:SantaVnodeHash",
],
)
@@ -118,7 +154,6 @@ objc_library(
":SNTDecisionCache",
":SNTRuleTable",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommon",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTFileInfo",
"//Source/common:SNTLogging",
@@ -161,7 +196,6 @@ objc_library(
hdrs = ["SNTPolicyProcessor.h"],
deps = [
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommon",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTFileInfo",
@@ -186,9 +220,9 @@ objc_library(
":SNTPolicyProcessor",
":SNTRuleTable",
":SNTSyncdQueue",
"//Source/common:BranchPrediction",
"//Source/common:SNTBlockMessage",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommon",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTDropRootPrivs",
@@ -197,6 +231,7 @@ objc_library(
"//Source/common:SNTMetricSet",
"//Source/common:SNTRule",
"//Source/common:SNTStoredEvent",
"//Source/common:SantaVnode",
"@MOLCodesignChecker",
],
)
@@ -210,6 +245,7 @@ objc_library(
":EndpointSecurityEnrichedTypes",
":EndpointSecurityMessage",
":Metrics",
":WatchItemPolicy",
],
)
@@ -224,9 +260,11 @@ objc_library(
":EndpointSecurityMessage",
":Metrics",
":SNTEndpointSecurityClientBase",
"//Source/common:SNTCommon",
":WatchItemPolicy",
"//Source/common:BranchPrediction",
"//Source/common:SNTConfigurator",
"//Source/common:SNTLogging",
"//Source/common:SystemResources",
],
)
@@ -245,8 +283,9 @@ objc_library(
":SNTCompilerController",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
"//Source/common:PrefixTree",
"//Source/common:SNTLogging",
"//Source/common:SNTPrefixTree",
"//Source/common:Unit",
],
)
@@ -261,6 +300,7 @@ objc_library(
":Metrics",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
":WatchItemPolicy",
"//Source/common:SNTLogging",
],
)
@@ -280,10 +320,42 @@ objc_library(
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
":SNTExecutionController",
"//Source/common:BranchPrediction",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTLogging",
],
)
objc_library(
name = "SNTEndpointSecurityFileAccessAuthorizer",
srcs = ["EventProviders/SNTEndpointSecurityFileAccessAuthorizer.mm"],
hdrs = ["EventProviders/SNTEndpointSecurityFileAccessAuthorizer.h"],
deps = [
":EndpointSecurityAPI",
":EndpointSecurityEnrichedTypes",
":EndpointSecurityEnricher",
":EndpointSecurityLogger",
":EndpointSecurityMessage",
":Metrics",
":RateLimiter",
":SNTDecisionCache",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
":WatchItemPolicy",
":WatchItems",
"//Source/common:Platform",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTMetricSet",
"//Source/common:SNTStrengthify",
"//Source/common:SantaCache",
"//Source/common:SantaVnode",
"//Source/common:SantaVnodeHash",
"@MOLCertificate",
"@MOLCodesignChecker",
],
)
objc_library(
name = "SNTEndpointSecurityDeviceManager",
srcs = ["EventProviders/SNTEndpointSecurityDeviceManager.mm"],
@@ -308,9 +380,22 @@ objc_library(
deps = [
":EndpointSecurityAPI",
":EndpointSecurityClient",
"//Source/common:SNTCommon",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTLogging",
"//Source/common:SantaCache",
"//Source/common:SantaVnode",
"//Source/common:SantaVnodeHash",
],
)
objc_library(
name = "RateLimiter",
srcs = ["EventProviders/RateLimiter.mm"],
hdrs = ["EventProviders/RateLimiter.h"],
deps = [
":Metrics",
"//Source/common:BranchPrediction",
"//Source/common:SystemResources",
],
)
@@ -341,6 +426,8 @@ objc_library(
":EndpointSecurityEnrichedTypes",
":EndpointSecurityMessage",
":SNTDecisionCache",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
],
)
@@ -356,6 +443,9 @@ objc_library(
":EndpointSecurityEnrichedTypes",
":EndpointSecurityMessage",
":SNTDecisionCache",
"//Source/common:SantaCache",
"//Source/common:SantaVnode",
"//Source/common:SantaVnodeHash",
],
)
@@ -365,6 +455,7 @@ objc_library(
hdrs = ["Logs/EndpointSecurity/Serializers/Empty.h"],
deps = [
":EndpointSecuritySerializer",
"//Source/common:SNTCachedDecision",
],
)
@@ -434,6 +525,7 @@ objc_library(
hdrs = ["Logs/EndpointSecurity/Writers/File.h"],
deps = [
":EndpointSecurityWriter",
"//Source/common:BranchPrediction",
],
)
@@ -492,6 +584,7 @@ objc_library(
hdrs = ["EventProviders/EndpointSecurity/Message.h"],
deps = [
":EndpointSecurityClient",
":WatchItemPolicy",
],
)
@@ -507,6 +600,8 @@ objc_library(
deps = [
":EndpointSecurityClient",
":EndpointSecurityMessage",
":WatchItemPolicy",
"//Source/common:Platform",
],
)
@@ -523,6 +618,7 @@ objc_library(
":SNTPolicyProcessor",
":SNTRuleTable",
":SNTSyncdQueue",
":WatchItems",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
@@ -564,20 +660,24 @@ objc_library(
":Metrics",
":SNTCompilerController",
":SNTDaemonControlController",
":SNTDecisionCache",
":SNTEndpointSecurityAuthorizer",
":SNTEndpointSecurityDeviceManager",
":SNTEndpointSecurityFileAccessAuthorizer",
":SNTEndpointSecurityRecorder",
":SNTEndpointSecurityTamperResistance",
":SNTExecutionController",
":SNTNotificationQueue",
":SNTSyncdQueue",
":WatchItems",
"//Source/common:PrefixTree",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTKVOManager",
"//Source/common:SNTLogging",
"//Source/common:SNTPrefixTree",
"//Source/common:SNTXPCNotifierInterface",
"//Source/common:SNTXPCSyncServiceInterface",
"//Source/common:Unit",
"@MOLXPCConnection",
],
)
@@ -599,12 +699,14 @@ objc_library(
":SNTNotificationQueue",
":SNTRuleTable",
":SNTSyncdQueue",
":WatchItems",
"//Source/common:PrefixTree",
"//Source/common:SNTConfigurator",
"//Source/common:SNTLogging",
"//Source/common:SNTMetricSet",
"//Source/common:SNTPrefixTree",
"//Source/common:SNTXPCControlInterface",
"//Source/common:SNTXPCUnprivilegedControlInterface",
"//Source/common:Unit",
"@MOLXPCConnection",
],
)
@@ -627,6 +729,7 @@ objc_library(
"//Source/common:SNTLogging",
"//Source/common:SNTMetricSet",
"//Source/common:SNTXPCControlInterface",
"//Source/common:SystemResources",
],
)
@@ -646,7 +749,7 @@ macos_bundle(
}),
infoplists = ["Info.plist"],
linkopts = ["-execute"],
minimum_os_version = "10.15",
minimum_os_version = "11.0",
provisioning_profile = select({
"//:adhoc_build": None,
"//conditions:default": "//profiles:daemon_dev",
@@ -671,6 +774,18 @@ objc_library(
":EndpointSecurityAPI",
":EndpointSecurityClient",
":EndpointSecurityMessage",
":WatchItemPolicy",
"@com_google_googletest//:gtest",
],
)
objc_library(
name = "MockLogger",
testonly = 1,
hdrs = ["Logs/EndpointSecurity/MockLogger.h"],
deps = [
":EndpointSecurityLogger",
":EndpointSecurityMessage",
"@com_google_googletest//:gtest",
],
)
@@ -707,6 +822,7 @@ santa_unit_test(
"EndpointSecurity",
],
deps = [
"//Source/common:Platform",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
@@ -725,7 +841,7 @@ santa_unit_test(
data = [
"//Source/santad/testdata:binaryrules_testdata",
],
minimum_os_version = "10.15",
minimum_os_version = "11.0",
sdk_dylibs = [
"bsm",
"EndpointSecurity",
@@ -753,9 +869,9 @@ santa_unit_test(
santa_unit_test(
name = "SNTApplicationCoreMetricsTest",
srcs = [
"SNTApplicationCoreMetricsTest.m",
"SNTApplicationCoreMetricsTest.mm",
],
minimum_os_version = "10.15",
minimum_os_version = "11.0",
deps = [
":SNTApplicationCoreMetrics",
"//Source/common:SNTCommonEnums",
@@ -852,13 +968,23 @@ santa_unit_test(
deps = [
":AuthResultCache",
":MockEndpointSecurityAPI",
"//Source/common:SNTCommon",
"//Source/common:SantaVnode",
"//Source/common:TestUtils",
"@OCMock",
"@com_google_googletest//:gtest",
],
)
santa_unit_test(
name = "RateLimiterTest",
srcs = ["EventProviders/RateLimiterTest.mm"],
deps = [
":Metrics",
":RateLimiter",
"//Source/common:SystemResources",
],
)
santa_unit_test(
name = "EndpointSecuritySerializerEmptyTest",
srcs = ["Logs/EndpointSecurity/Serializers/EmptyTest.mm"],
@@ -982,7 +1108,6 @@ santa_unit_test(
":SNTDecisionCache",
":SNTRuleTable",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommon",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTRule",
"//Source/common:TestUtils",
@@ -1004,6 +1129,8 @@ santa_unit_test(
":Metrics",
":MockEndpointSecurityAPI",
":SNTEndpointSecurityClient",
":WatchItemPolicy",
"//Source/common:SNTConfigurator",
"//Source/common:TestUtils",
"@OCMock",
"@com_google_googletest//:gtest",
@@ -1026,7 +1153,6 @@ santa_unit_test(
":SNTExecutionController",
":SNTRuleTable",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommon",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTFileInfo",
@@ -1054,12 +1180,39 @@ santa_unit_test(
":SNTCompilerController",
":SNTEndpointSecurityAuthorizer",
":SNTExecutionController",
"//Source/common:SNTCommonEnums",
"//Source/common:TestUtils",
"@OCMock",
"@com_google_googletest//:gtest",
],
)
santa_unit_test(
name = "SNTEndpointSecurityFileAccessAuthorizerTest",
srcs = ["EventProviders/SNTEndpointSecurityFileAccessAuthorizerTest.mm"],
sdk_dylibs = [
"EndpointSecurity",
],
deps = [
":EndpointSecurityLogger",
":EndpointSecurityMessage",
":MockEndpointSecurityAPI",
":MockLogger",
":SNTDecisionCache",
":SNTEndpointSecurityFileAccessAuthorizer",
":WatchItemPolicy",
":WatchItems",
"//Source/common:Platform",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTConfigurator",
"//Source/common:TestUtils",
"@MOLCertificate",
"@MOLCodesignChecker",
"@OCMock",
"@com_google_googletest//:gtest",
],
)
santa_unit_test(
name = "SNTEndpointSecurityTamperResistanceTest",
srcs = ["EventProviders/SNTEndpointSecurityTamperResistanceTest.mm"],
@@ -1072,6 +1225,7 @@ santa_unit_test(
":Metrics",
":MockEndpointSecurityAPI",
":SNTEndpointSecurityTamperResistance",
":WatchItemPolicy",
"//Source/common:SNTLogging",
"//Source/common:TestUtils",
"@OCMock",
@@ -1096,7 +1250,9 @@ santa_unit_test(
":MockEndpointSecurityAPI",
":SNTCompilerController",
":SNTEndpointSecurityRecorder",
"//Source/common:PrefixTree",
"//Source/common:TestUtils",
"//Source/common:Unit",
"@OCMock",
"@com_google_googletest//:gtest",
],
@@ -1161,18 +1317,21 @@ test_suite(
":EndpointSecurityWriterFileTest",
":EndpointSecurityWriterSpoolTest",
":MetricsTest",
":RateLimiterTest",
":SNTApplicationCoreMetricsTest",
":SNTCompilerControllerTest",
":SNTDecisionCacheTest",
":SNTEndpointSecurityAuthorizerTest",
":SNTEndpointSecurityClientTest",
":SNTEndpointSecurityDeviceManagerTest",
":SNTEndpointSecurityFileAccessAuthorizerTest",
":SNTEndpointSecurityRecorderTest",
":SNTEndpointSecurityTamperResistanceTest",
":SNTEventTableTest",
":SNTExecutionControllerTest",
":SNTRuleTableTest",
":SantadTest",
":WatchItemsTest",
"//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:fsspool_test",
],
visibility = ["//:santa_package_group"],

View File

@@ -18,6 +18,7 @@
#import <MOLCertificate/MOLCertificate.h>
#import <MOLCodesignChecker/MOLCodesignChecker.h>
#import "Source/common/Platform.h"
#import "Source/common/SNTCachedDecision.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTFileInfo.h"
@@ -36,7 +37,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
// instead we use the following preprocessor macros to conditionally compile these API. The
// drawback here is that if a pre-macOS 12 SDK is used to build Santa and it is then deployed
// on macOS 12 or later, the dynamic mute set will not be computed.
#if defined(MAC_OS_VERSION_12_0) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_12_0
#if HAVE_MACOS_12
// Create a temporary ES client in order to grab the default set of muted paths.
// TODO(mlw): Reorganize this code so that a temporary ES client doesn't need to be created
es_client_t *client = NULL;

View File

@@ -45,7 +45,7 @@
- (SNTRule *)_exampleBinaryRule {
SNTRule *r = [[SNTRule alloc] init];
r.identifier = @"a";
r.identifier = @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670";
r.state = SNTRuleStateBlock;
r.type = SNTRuleTypeBinary;
r.customMsg = @"A rule";
@@ -54,7 +54,7 @@
- (SNTRule *)_exampleCertRule {
SNTRule *r = [[SNTRule alloc] init];
r.identifier = @"b";
r.identifier = @"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258";
r.state = SNTRuleStateAllow;
r.type = SNTRuleTypeCertificate;
return r;
@@ -112,7 +112,7 @@
- (void)testAddInvalidRule {
SNTRule *r = [[SNTRule alloc] init];
r.identifier = @"a";
r.identifier = @"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258";
r.type = SNTRuleTypeCertificate;
NSError *error;
@@ -125,12 +125,19 @@
cleanSlate:NO
error:nil];
SNTRule *r = [self.sut ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil];
SNTRule *r = [self.sut
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
certificateSHA256:nil
teamID:nil];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"a");
XCTAssertEqualObjects(r.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
XCTAssertEqual(r.type, SNTRuleTypeBinary);
r = [self.sut ruleForBinarySHA256:@"b" certificateSHA256:nil teamID:nil];
r = [self.sut
ruleForBinarySHA256:@"b6ee1c3c5a715c049d14a8457faa6b6701b8507efe908300e238e0768bd759c2"
certificateSHA256:nil
teamID:nil];
XCTAssertNil(r);
}
@@ -139,12 +146,19 @@
cleanSlate:NO
error:nil];
SNTRule *r = [self.sut ruleForBinarySHA256:nil certificateSHA256:@"b" teamID:nil];
SNTRule *r = [self.sut
ruleForBinarySHA256:nil
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
teamID:nil];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"b");
XCTAssertEqualObjects(r.identifier,
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
XCTAssertEqual(r.type, SNTRuleTypeCertificate);
r = [self.sut ruleForBinarySHA256:nil certificateSHA256:@"a" teamID:nil];
r = [self.sut
ruleForBinarySHA256:nil
certificateSHA256:@"5bdab1288fc16892fef50c658db54f1e2e19cf8f71cc55f77de2b95e051e2562"
teamID:nil];
XCTAssertNil(r);
}
@@ -171,19 +185,31 @@
// This test verifies that the implicit rule ordering we've been abusing is still working.
// See the comment in SNTRuleTable#ruleForBinarySHA256:certificateSHA256:teamID
SNTRule *r = [self.sut ruleForBinarySHA256:@"a" certificateSHA256:@"b" teamID:@"teamID"];
SNTRule *r = [self.sut
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
teamID:@"teamID"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"a");
XCTAssertEqualObjects(r.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
XCTAssertEqual(r.type, SNTRuleTypeBinary, @"Implicit rule ordering failed");
r = [self.sut ruleForBinarySHA256:@"a" certificateSHA256:@"unknowncert" teamID:@"teamID"];
r = [self.sut
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
certificateSHA256:@"unknowncert"
teamID:@"teamID"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"a");
XCTAssertEqualObjects(r.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
XCTAssertEqual(r.type, SNTRuleTypeBinary, @"Implicit rule ordering failed");
r = [self.sut ruleForBinarySHA256:@"unknown" certificateSHA256:@"b" teamID:@"teamID"];
r = [self.sut
ruleForBinarySHA256:@"unknown"
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
teamID:@"teamID"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"b");
XCTAssertEqualObjects(r.identifier,
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
XCTAssertEqual(r.type, SNTRuleTypeCertificate, @"Implicit rule ordering failed");
}

View File

@@ -0,0 +1,101 @@
/// Copyright 2022 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__DATALAYER_WATCHITEMPOLICY_H
#define SANTA__SANTAD__DATALAYER_WATCHITEMPOLICY_H
#include <Kernel/kern/cs_blobs.h>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace santa::santad::data_layer {
enum class WatchItemPathType {
kPrefix,
kLiteral,
};
static constexpr WatchItemPathType kWatchItemPolicyDefaultPathType =
WatchItemPathType::kLiteral;
static constexpr bool kWatchItemPolicyDefaultAllowReadAccess = false;
static constexpr bool kWatchItemPolicyDefaultAuditOnly = true;
struct WatchItemPolicy {
struct Process {
Process(std::string bp, std::string sid, std::string ti,
std::vector<uint8_t> cdh, std::string ch, std::optional<bool> pb)
: binary_path(bp),
signing_id(sid),
team_id(ti),
cdhash(std::move(cdh)),
certificate_sha256(ch),
platform_binary(pb) {}
bool operator==(const Process &other) const {
return binary_path == other.binary_path &&
signing_id == other.signing_id && team_id == other.team_id &&
cdhash == other.cdhash &&
certificate_sha256 == other.certificate_sha256 &&
platform_binary.has_value() == other.platform_binary.has_value() &&
platform_binary.value_or(false) ==
other.platform_binary.value_or(false);
}
bool operator!=(const Process &other) const { return !(*this == other); }
std::string binary_path;
std::string signing_id;
std::string team_id;
std::vector<uint8_t> cdhash;
std::string certificate_sha256;
std::optional<bool> platform_binary;
};
WatchItemPolicy(std::string_view n, std::string_view p,
WatchItemPathType pt = kWatchItemPolicyDefaultPathType,
bool ara = kWatchItemPolicyDefaultAllowReadAccess,
bool ao = kWatchItemPolicyDefaultAuditOnly,
std::vector<Process> procs = {})
: name(n),
path(p),
path_type(pt),
allow_read_access(ara),
audit_only(ao),
processes(std::move(procs)) {}
bool operator==(const WatchItemPolicy &other) const {
return name == other.name && path == other.path &&
path_type == other.path_type &&
allow_read_access == other.allow_read_access &&
audit_only == other.audit_only && processes == other.processes;
}
bool operator!=(const WatchItemPolicy &other) const {
return !(*this == other);
}
std::string name;
std::string path;
WatchItemPathType path_type;
bool allow_read_access;
bool audit_only;
std::vector<Process> processes;
};
} // namespace santa::santad::data_layer
#endif

View File

@@ -0,0 +1,131 @@
/// Copyright 2022 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__DATALAYER_WATCHITEMS_H
#define SANTA__SANTAD__DATALAYER_WATCHITEMS_H
#include <CommonCrypto/CommonDigest.h>
#import <Foundation/Foundation.h>
#include <dispatch/dispatch.h>
#include <array>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "Source/common/PrefixTree.h"
#include "Source/santad/DataLayer/WatchItemPolicy.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h"
extern NSString *const kWatchItemConfigKeyVersion;
extern NSString *const kWatchItemConfigKeyWatchItems;
extern NSString *const kWatchItemConfigKeyPaths;
extern NSString *const kWatchItemConfigKeyPathsPath;
extern NSString *const kWatchItemConfigKeyPathsIsPrefix;
extern NSString *const kWatchItemConfigKeyOptions;
extern NSString *const kWatchItemConfigKeyOptionsAllowReadAccess;
extern NSString *const kWatchItemConfigKeyOptionsAuditOnly;
extern NSString *const kWatchItemConfigKeyProcesses;
extern NSString *const kWatchItemConfigKeyProcessesBinaryPath;
extern NSString *const kWatchItemConfigKeyProcessesCertificateSha256;
extern NSString *const kWatchItemConfigKeyProcessesSigningID;
extern NSString *const kWatchItemConfigKeyProcessesTeamID;
extern NSString *const kWatchItemConfigKeyProcessesCDHash;
extern NSString *const kWatchItemConfigKeyProcessesPlatformBinary;
// Forward declarations
namespace santa::santad::data_layer {
class WatchItemsPeer;
}
namespace santa::santad::data_layer {
struct WatchItemsState {
uint64_t rule_count;
NSString *policy_version;
NSString *config_path;
NSTimeInterval last_config_load_epoch;
};
class WatchItems : public std::enable_shared_from_this<WatchItems> {
public:
using VersionAndPolicies =
std::pair<std::string, std::vector<std::optional<std::shared_ptr<WatchItemPolicy>>>>;
using WatchItemsTree = santa::common::PrefixTree<std::shared_ptr<WatchItemPolicy>>;
// Factory
static std::shared_ptr<WatchItems> Create(NSString *config_path,
uint64_t reapply_config_frequency_secs);
// Factory
static std::shared_ptr<WatchItems> Create(NSDictionary *config,
uint64_t reapply_config_frequency_secs);
WatchItems(NSString *config_path, dispatch_queue_t q, dispatch_source_t timer_source,
void (^periodic_task_complete_f)(void) = nullptr);
WatchItems(NSDictionary *config, dispatch_queue_t q, dispatch_source_t timer_source,
void (^periodic_task_complete_f)(void) = nullptr);
~WatchItems();
void BeginPeriodicTask();
void RegisterClient(id<SNTEndpointSecurityDynamicEventHandler> client);
void SetConfigPath(NSString *config_path);
void SetConfig(NSDictionary *config);
VersionAndPolicies FindPolciesForPaths(const std::vector<std::string_view> &paths);
std::optional<WatchItemsState> State();
friend class santa::santad::data_layer::WatchItemsPeer;
private:
static std::shared_ptr<WatchItems> CreateInternal(NSString *config_path, NSDictionary *config,
uint64_t reapply_config_frequency_secs);
NSDictionary *ReadConfig();
NSDictionary *ReadConfigLocked() ABSL_SHARED_LOCKS_REQUIRED(lock_);
void ReloadConfig(NSDictionary *new_config);
void UpdateCurrentState(std::unique_ptr<WatchItemsTree> new_tree,
std::set<std::pair<std::string, WatchItemPathType>> &&new_monitored_paths,
NSDictionary *new_config);
bool BuildPolicyTree(const std::vector<std::shared_ptr<WatchItemPolicy>> &watch_items,
WatchItemsTree &tree,
std::set<std::pair<std::string, WatchItemPathType>> &paths);
NSString *config_path_;
NSDictionary *embedded_config_;
dispatch_queue_t q_;
dispatch_source_t timer_source_;
void (^periodic_task_complete_f_)(void);
absl::Mutex lock_;
std::unique_ptr<WatchItemsTree> watch_items_ ABSL_GUARDED_BY(lock_);
NSDictionary *current_config_ ABSL_GUARDED_BY(lock_);
NSTimeInterval last_update_time_ ABSL_GUARDED_BY(lock_);
std::set<std::pair<std::string, WatchItemPathType>> currently_monitored_paths_
ABSL_GUARDED_BY(lock_);
std::string policy_version_ ABSL_GUARDED_BY(lock_);
std::set<id<SNTEndpointSecurityDynamicEventHandler>> registerd_clients_ ABSL_GUARDED_BY(lock_);
bool periodic_task_started_ = false;
};
} // namespace santa::santad::data_layer
#endif

View File

@@ -0,0 +1,782 @@
/// Copyright 2022 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/DataLayer/WatchItems.h"
#include <CommonCrypto/CommonDigest.h>
#include <Kernel/kern/cs_blobs.h>
#include <ctype.h>
#include <glob.h>
#include <sys/syslimits.h>
#include <algorithm>
#include <cstddef>
#include <cstdlib>
#include <iterator>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <utility>
#include <variant>
#include <vector>
#import "Source/common/PrefixTree.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/Unit.h"
#include "Source/santad/DataLayer/WatchItemPolicy.h"
using santa::common::PrefixTree;
using santa::common::Unit;
using santa::santad::data_layer::WatchItemPathType;
using santa::santad::data_layer::WatchItemPolicy;
NSString *const kWatchItemConfigKeyVersion = @"Version";
NSString *const kWatchItemConfigKeyWatchItems = @"WatchItems";
NSString *const kWatchItemConfigKeyPaths = @"Paths";
NSString *const kWatchItemConfigKeyPathsPath = @"Path";
NSString *const kWatchItemConfigKeyPathsIsPrefix = @"IsPrefix";
NSString *const kWatchItemConfigKeyOptions = @"Options";
NSString *const kWatchItemConfigKeyOptionsAllowReadAccess = @"AllowReadAccess";
NSString *const kWatchItemConfigKeyOptionsAuditOnly = @"AuditOnly";
NSString *const kWatchItemConfigKeyProcesses = @"Processes";
NSString *const kWatchItemConfigKeyProcessesBinaryPath = @"BinaryPath";
NSString *const kWatchItemConfigKeyProcessesCertificateSha256 = @"CertificateSha256";
NSString *const kWatchItemConfigKeyProcessesSigningID = @"SigningID";
NSString *const kWatchItemConfigKeyProcessesTeamID = @"TeamID";
NSString *const kWatchItemConfigKeyProcessesCDHash = @"CDHash";
NSString *const kWatchItemConfigKeyProcessesPlatformBinary = @"PlatformBinary";
// https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/
static constexpr NSUInteger kMaxTeamIDLength = 10;
// Semi-arbitrary upper bound.
static constexpr NSUInteger kMaxSigningIDLength = 512;
// Semi-arbitrary minimum allowed reapplication frequency.
// Goal is to prevent a configuration setting that would cause too much
// churn rebuilding glob paths based on the state of the filesystem.
static constexpr uint64_t kMinReapplyConfigFrequencySecs = 15;
namespace santa::santad::data_layer {
// Type aliases
using ValidatorBlock = bool (^)(id, NSError **);
using PathAndTypePair = std::pair<std::string, WatchItemPathType>;
using PathList = std::vector<PathAndTypePair>;
using ProcessList = std::vector<WatchItemPolicy::Process>;
static void PopulateError(NSError **err, NSString *msg) {
if (err) {
*err = [NSError errorWithDomain:@"com.google.santa.watchitems"
code:0
userInfo:@{NSLocalizedDescriptionKey : msg}];
}
}
/// Ensure the given string has the expected length and only
/// contains valid hex digits
bool ConfirmValidHexString(NSString *str, size_t expected_length) {
if (str.length != expected_length) {
return false;
}
for (int i = 0; i < str.length; i++) {
if (!isxdigit([str characterAtIndex:i])) {
return false;
}
}
return true;
}
static std::vector<uint8_t> HexStringToBytes(NSString *str) {
if (!str) {
return std::vector<uint8_t>{};
}
std::vector<uint8_t> bytes;
bytes.reserve(str.length / 2);
char cur_byte[3];
cur_byte[2] = '\0';
for (int i = 0; i < str.length / 2; i++) {
cur_byte[0] = [str characterAtIndex:(i * 2)];
cur_byte[1] = [str characterAtIndex:(i * 2 + 1)];
bytes.push_back(std::strtoul(cur_byte, nullptr, 16));
}
return bytes;
}
// Given a length, returns a ValidatorBlock that confirms the
// string is a valid hex string of the given length.
ValidatorBlock HexValidator(NSUInteger expected_length) {
return ^bool(NSString *val, NSError **err) {
if (!ConfirmValidHexString(val, expected_length)) {
PopulateError(
err, [NSString stringWithFormat:@"Expected hex string of length %lu", expected_length]);
return false;
}
return true;
};
}
// Given a max length, returns a ValidatorBlock that confirms the
// string is a not longer than the max.
ValidatorBlock LenRangeValidator(NSUInteger min_length, NSUInteger max_length) {
return ^bool(NSString *val, NSError **err) {
if (val.length < min_length) {
PopulateError(err, [NSString stringWithFormat:@"Value too short. Got: %lu, Min: %lu",
val.length, min_length]);
return false;
} else if (val.length > max_length) {
PopulateError(err, [NSString stringWithFormat:@"Value too long. Got: %lu, Max: %lu",
val.length, max_length]);
return false;
}
return true;
};
}
/// Ensure the key exists (if required) and the value matches the expected type
bool VerifyConfigKey(NSDictionary *dict, const NSString *key, Class expected, NSError **err,
bool required = false, bool (^Validator)(id, NSError **) = nil) {
if (dict[key]) {
if (![dict[key] isKindOfClass:expected]) {
PopulateError(err, [NSString stringWithFormat:@"Expected type '%@' for key '%@' (got: %@)",
NSStringFromClass(expected), key,
NSStringFromClass([dict[key] class])]);
return false;
}
NSError *validator_err;
if (Validator && !Validator(dict[key], &validator_err)) {
PopulateError(err, [NSString stringWithFormat:@"Invalid content in key '%@': %@", key,
validator_err.localizedDescription]);
return false;
}
} else if (required) {
PopulateError(err, [NSString stringWithFormat:@"Missing required key '%@'", key]);
return false;
}
return true;
}
/// Ensure all values of the array key in the dictionary conform to the
/// expected type. If a Validator block is supplied, each item is also
/// subject to the custom validation method.
bool VerifyConfigKeyArray(NSDictionary *dict, NSString *key, Class expected, NSError **err,
bool (^Validator)(id, NSError **) = nil) {
if (!VerifyConfigKey(dict, key, [NSArray class], err)) {
return false;
}
__block bool success = true;
__block NSError *block_err;
[dict[key] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if (![obj isKindOfClass:expected]) {
success = false;
PopulateError(&block_err,
[NSString stringWithFormat:@"Expected all '%@' types in array key '%@'",
NSStringFromClass(expected), key]);
*stop = YES;
return;
}
NSError *validator_err;
if (Validator && !Validator(obj, &validator_err)) {
PopulateError(&block_err,
[NSString stringWithFormat:@"Invalid content in array key '%@': %@", key,
validator_err.localizedDescription]);
success = false;
*stop = YES;
return;
}
}];
if (!success && block_err) {
PopulateError(err, block_err.localizedDescription);
}
return success;
}
/// The `Paths` array can contain only `string` and `dict` types:
/// - For `string` types, the default path type `kDefaultPathType` is used
/// - For `dict` types, there is a required `Path` key. and an optional
/// `IsPrefix` key to set the path type to something other than the default
///
/// Example:
/// <array>
/// <string>/my/path</string>
/// <dict>
/// <key>Path</key>
/// <string>/another/partial/path</string>
/// <key>IsPrefix</key>
/// <true/>
/// </dict>
/// </array>
std::variant<Unit, PathList> VerifyConfigWatchItemPaths(NSArray<id> *paths, NSError **err) {
PathList path_list;
for (id path in paths) {
if ([path isKindOfClass:[NSDictionary class]]) {
NSDictionary *path_dict = (NSDictionary *)path;
if (!VerifyConfigKey(path_dict, kWatchItemConfigKeyPathsPath, [NSString class], err, true,
LenRangeValidator(1, PATH_MAX))) {
return Unit{};
}
NSString *path_str = path_dict[kWatchItemConfigKeyPathsPath];
WatchItemPathType path_type = kWatchItemPolicyDefaultPathType;
if (VerifyConfigKey(path_dict, kWatchItemConfigKeyPathsIsPrefix, [NSNumber class], err)) {
path_type = ([(NSNumber *)path_dict[kWatchItemConfigKeyPathsIsPrefix] boolValue] == NO
? WatchItemPathType::kLiteral
: WatchItemPathType::kPrefix);
} else {
return Unit{};
}
path_list.push_back({std::string(path_str.UTF8String, path_str.length), path_type});
} else if ([path isKindOfClass:[NSString class]]) {
if (!LenRangeValidator(1, PATH_MAX)(path, err)) {
PopulateError(err, [NSString stringWithFormat:@"Invalid path length: %@",
(err && *err) ? (*err).localizedDescription
: @"Unknown error"]);
return Unit{};
}
path_list.push_back({std::string(((NSString *)path).UTF8String, ((NSString *)path).length),
kWatchItemPolicyDefaultPathType});
} else {
PopulateError(
err, [NSString stringWithFormat:
@"%@ array item with invalid type. Expected 'dict' or 'string' (got: %@)",
kWatchItemConfigKeyPaths, NSStringFromClass([path class])]);
return Unit{};
}
}
if (path_list.size() == 0) {
PopulateError(err, [NSString stringWithFormat:@"No paths specified"]);
return Unit{};
}
return path_list;
}
/// The `Processes` array can only contain dictionaries. Each dictionary can
/// contain the attributes that describe a single process.
///
/// <array>
/// <dict>
/// <key>BinaryPath</key>
/// <string>AAAA</string>
/// <key>TeamID</key>
/// <string>BBBB</string>
/// <key>PlatformBinary</key>
/// <true/>
/// </dict>
/// <dict>
/// <key>CertificateSha256</key>
/// <string>CCCC</string>
/// <key>CDHash</key>
/// <string>DDDD</string>
/// <key>SigningID</key>
/// <string>EEEE</string>
/// </dict>
/// </array>
std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *watch_item,
NSError **err) {
__block ProcessList proc_list;
if (!VerifyConfigKeyArray(
watch_item, kWatchItemConfigKeyProcesses, [NSDictionary class], err,
^bool(NSDictionary *process, NSError **err) {
if (!VerifyConfigKey(process, kWatchItemConfigKeyProcessesBinaryPath, [NSString class],
err, false, LenRangeValidator(1, PATH_MAX)) ||
!VerifyConfigKey(process, kWatchItemConfigKeyProcessesSigningID, [NSString class],
err, false, LenRangeValidator(1, kMaxSigningIDLength)) ||
!VerifyConfigKey(process, kWatchItemConfigKeyProcessesTeamID, [NSString class], err,
false, LenRangeValidator(kMaxTeamIDLength, kMaxTeamIDLength)) ||
!VerifyConfigKey(process, kWatchItemConfigKeyProcessesCDHash, [NSString class], err,
false, HexValidator(CS_CDHASH_LEN * 2)) ||
!VerifyConfigKey(process, kWatchItemConfigKeyProcessesCertificateSha256,
[NSString class], err, false,
HexValidator(CC_SHA256_DIGEST_LENGTH * 2)) ||
!VerifyConfigKey(process, kWatchItemConfigKeyProcessesPlatformBinary,
[NSNumber class], err, false, nil)) {
PopulateError(err, @"Failed to verify key content");
return false;
}
// Ensure at least one attribute set
if (!process[kWatchItemConfigKeyProcessesBinaryPath] &&
!process[kWatchItemConfigKeyProcessesSigningID] &&
!process[kWatchItemConfigKeyProcessesTeamID] &&
!process[kWatchItemConfigKeyProcessesCDHash] &&
!process[kWatchItemConfigKeyProcessesCertificateSha256] &&
!process[kWatchItemConfigKeyProcessesPlatformBinary]) {
PopulateError(err, @"No valid attributes set in process dictionary");
return false;
}
proc_list.push_back(WatchItemPolicy::Process(
std::string([(process[kWatchItemConfigKeyProcessesBinaryPath] ?: @"") UTF8String]),
std::string([(process[kWatchItemConfigKeyProcessesSigningID] ?: @"") UTF8String]),
std::string([(process[kWatchItemConfigKeyProcessesTeamID] ?: @"") UTF8String]),
HexStringToBytes(process[kWatchItemConfigKeyProcessesCDHash]),
std::string(
[(process[kWatchItemConfigKeyProcessesCertificateSha256] ?: @"") UTF8String]),
process[kWatchItemConfigKeyProcessesPlatformBinary]
? std::make_optional(
(bool)[process[kWatchItemConfigKeyProcessesPlatformBinary] boolValue])
: std::nullopt));
return true;
})) {
return Unit{};
}
return proc_list;
}
/// Ensure that a given watch item conforms to expected structure
///
/// Example:
/// <dict>
/// <key>Paths</key>
/// <array>
/// ... See VerifyConfigWatchItemPaths for more details ...
/// </array>
/// <key>Options</key>
/// <dict>
/// <key>AllowReadAccess</key>
/// <false/>
/// <key>AuditOnly</key>
/// <false/>
/// </dict>
/// <key>Processes</key>
/// <array>
/// ... See VerifyConfigWatchItemProcesses for more details ...
/// </array>
/// </dict>
bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
std::vector<std::shared_ptr<WatchItemPolicy>> &policies,
NSError **err) {
if (!VerifyConfigKey(watch_item, kWatchItemConfigKeyPaths, [NSArray class], err, true)) {
return false;
}
std::variant<Unit, PathList> path_list =
VerifyConfigWatchItemPaths(watch_item[kWatchItemConfigKeyPaths], err);
if (std::holds_alternative<Unit>(path_list)) {
return false;
}
if (!VerifyConfigKey(watch_item, kWatchItemConfigKeyOptions, [NSDictionary class], err)) {
return false;
}
NSDictionary *options = watch_item[kWatchItemConfigKeyOptions];
if (options) {
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsAllowReadAccess, [NSNumber class],
err)) {
return false;
}
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsAuditOnly, [NSNumber class], err)) {
return false;
}
}
bool allow_read_access = options[kWatchItemConfigKeyOptionsAllowReadAccess]
? [options[kWatchItemConfigKeyOptionsAllowReadAccess] boolValue]
: kWatchItemPolicyDefaultAllowReadAccess;
bool audit_only = options[kWatchItemConfigKeyOptionsAuditOnly]
? [options[kWatchItemConfigKeyOptionsAuditOnly] boolValue]
: kWatchItemPolicyDefaultAuditOnly;
std::variant<Unit, ProcessList> proc_list = VerifyConfigWatchItemProcesses(watch_item, err);
if (std::holds_alternative<Unit>(proc_list)) {
return false;
}
for (const PathAndTypePair &path_type_pair : std::get<PathList>(path_list)) {
policies.push_back(std::make_shared<WatchItemPolicy>(
[name UTF8String], path_type_pair.first, path_type_pair.second, allow_read_access, audit_only,
std::get<ProcessList>(proc_list)));
}
return true;
}
bool IsWatchItemNameValid(NSString *watch_item_name, NSError **err) {
if (!watch_item_name) {
// This shouldn't be possible as written, but handle just in case
PopulateError(err, [NSString stringWithFormat:@"nil watch item name"]);
return false;
}
static dispatch_once_t once_token;
static NSRegularExpression *regex;
dispatch_once(&once_token, ^{
// Should only match legal C identifiers
regex = [NSRegularExpression regularExpressionWithPattern:@"^[A-Za-z_][A-Za-z0-9_]*$"
options:0
error:nil];
});
if ([regex numberOfMatchesInString:watch_item_name
options:0
range:NSMakeRange(0, watch_item_name.length)] != 1) {
PopulateError(err, [NSString stringWithFormat:@"Key name must match regular expression \"%@\"",
regex.pattern]);
return false;
}
return true;
}
bool ParseConfig(NSDictionary *config, std::vector<std::shared_ptr<WatchItemPolicy>> &policies,
NSError **err) {
if (![config[kWatchItemConfigKeyVersion] isKindOfClass:[NSString class]]) {
PopulateError(err, [NSString stringWithFormat:@"Missing top level string key '%@'",
kWatchItemConfigKeyVersion]);
return false;
}
if ([(NSString *)config[kWatchItemConfigKeyVersion] length] == 0) {
PopulateError(err, [NSString stringWithFormat:@"Top level key '%@' has empty value",
kWatchItemConfigKeyVersion]);
return false;
}
if (config[kWatchItemConfigKeyWatchItems] &&
![config[kWatchItemConfigKeyWatchItems] isKindOfClass:[NSDictionary class]]) {
PopulateError(err, [NSString stringWithFormat:@"Top level key '%@' must be a dictionary",
kWatchItemConfigKeyWatchItems]);
return false;
}
NSDictionary *watch_items = config[kWatchItemConfigKeyWatchItems];
for (id key in watch_items) {
if (![key isKindOfClass:[NSString class]]) {
PopulateError(err,
[NSString stringWithFormat:@"Invalid %@ key %@: Expected type '%@' (got: %@)",
kWatchItemConfigKeyWatchItems, key,
NSStringFromClass([NSString class]),
NSStringFromClass([key class])]);
return false;
}
if (!IsWatchItemNameValid((NSString *)key, err)) {
PopulateError(
err, [NSString
stringWithFormat:@"Invalid %@ key '%@': %@", kWatchItemConfigKeyWatchItems, key,
(err && *err) ? (*err).localizedDescription : @"Unknown failure"]);
return false;
}
if (![watch_items[key] isKindOfClass:[NSDictionary class]]) {
PopulateError(
err,
[NSString stringWithFormat:@"Value type for watch item '%@' must be a dictionary (got %@)",
key, NSStringFromClass([watch_items[key] class])]);
return false;
}
if (!ParseConfigSingleWatchItem(key, watch_items[key], policies, err)) {
PopulateError(err, [NSString stringWithFormat:@"In watch item '%@': %@", key,
(err && *err) ? (*err).localizedDescription
: @"Unknown failure"]);
return false;
}
}
return true;
}
std::shared_ptr<WatchItems> WatchItems::Create(NSString *config_path,
uint64_t reapply_config_frequency_secs) {
return CreateInternal(config_path, nil, reapply_config_frequency_secs);
}
std::shared_ptr<WatchItems> WatchItems::Create(NSDictionary *config,
uint64_t reapply_config_frequency_secs) {
return CreateInternal(nil, config, reapply_config_frequency_secs);
}
std::shared_ptr<WatchItems> WatchItems::CreateInternal(NSString *config_path, NSDictionary *config,
uint64_t reapply_config_frequency_secs) {
if (reapply_config_frequency_secs < kMinReapplyConfigFrequencySecs) {
LOGW(@"Invalid watch item update interval provided: %llu. Min allowed: %llu",
reapply_config_frequency_secs, kMinReapplyConfigFrequencySecs);
return nullptr;
}
if (config_path && config) {
LOGW(@"Invalid arguments creating WatchItems - both config and config_path cannot be set.");
return nullptr;
}
dispatch_queue_t q = dispatch_queue_create("com.google.santa.daemon.watch_items.q",
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
dispatch_source_t timer_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
dispatch_source_set_timer(timer_source, dispatch_time(DISPATCH_TIME_NOW, 0),
NSEC_PER_SEC * reapply_config_frequency_secs, 0);
if (config_path) {
return std::make_shared<WatchItems>(config_path, q, timer_source);
} else {
return std::make_shared<WatchItems>(config, q, timer_source);
}
}
WatchItems::WatchItems(NSString *config_path, dispatch_queue_t q, dispatch_source_t timer_source,
void (^periodic_task_complete_f)(void))
: config_path_(config_path),
embedded_config_(nil),
q_(q),
timer_source_(timer_source),
periodic_task_complete_f_(periodic_task_complete_f),
watch_items_(std::make_unique<WatchItemsTree>()) {}
WatchItems::WatchItems(NSDictionary *config, dispatch_queue_t q, dispatch_source_t timer_source,
void (^periodic_task_complete_f)(void))
: config_path_(nil),
embedded_config_(config),
q_(q),
timer_source_(timer_source),
periodic_task_complete_f_(periodic_task_complete_f),
watch_items_(std::make_unique<WatchItemsTree>()) {}
WatchItems::~WatchItems() {
if (!periodic_task_started_ && timer_source_ != NULL) {
// The timer_source_ must be resumed to ensure it has a proper retain count before being
// destroyed. Additionally, it should first be cancelled to ensure the timer isn't ever
// fired (see man page for `dispatch_source_cancel(3)`).
dispatch_source_cancel(timer_source_);
dispatch_resume(timer_source_);
}
}
bool WatchItems::BuildPolicyTree(const std::vector<std::shared_ptr<WatchItemPolicy>> &watch_items,
PrefixTree<std::shared_ptr<WatchItemPolicy>> &tree,
std::set<std::pair<std::string, WatchItemPathType>> &paths) {
glob_t *g = (glob_t *)alloca(sizeof(glob_t));
for (const std::shared_ptr<WatchItemPolicy> &item : watch_items) {
int err = glob(item->path.c_str(), 0, nullptr, g);
if (err != 0 && err != GLOB_NOMATCH) {
LOGE(@"Failed to generate path names for watch item: %s", item->name.c_str());
return false;
}
for (size_t i = g->gl_offs; i < g->gl_pathc; i++) {
if (item->path_type == WatchItemPathType::kPrefix) {
tree.InsertPrefix(g->gl_pathv[i], item);
} else {
tree.InsertLiteral(g->gl_pathv[i], item);
}
paths.insert({g->gl_pathv[i], item->path_type});
}
globfree(g);
}
return true;
}
void WatchItems::RegisterClient(id<SNTEndpointSecurityDynamicEventHandler> client) {
absl::MutexLock lock(&lock_);
registerd_clients_.insert(client);
}
void WatchItems::UpdateCurrentState(
std::unique_ptr<PrefixTree<std::shared_ptr<WatchItemPolicy>>> new_tree,
std::set<std::pair<std::string, WatchItemPathType>> &&new_monitored_paths,
NSDictionary *new_config) {
absl::MutexLock lock(&lock_);
// The following conditions require updating the current config:
// 1. The current config doesn't exist but the new one does
// 2. The current config exists but the new one doesn't
// 3. The set of monitored paths changed
// 4. The configuration changed
if ((current_config_ != nil && new_config == nil) ||
(current_config_ == nil && new_config != nil) ||
(currently_monitored_paths_ != new_monitored_paths) ||
(new_config && ![current_config_ isEqualToDictionary:new_config])) {
std::vector<std::pair<std::string, WatchItemPathType>> paths_to_watch;
std::vector<std::pair<std::string, WatchItemPathType>> paths_to_stop_watching;
// New paths to watch are those that are in the new set, but not current
std::set_difference(new_monitored_paths.begin(), new_monitored_paths.end(),
currently_monitored_paths_.begin(), currently_monitored_paths_.end(),
std::back_inserter(paths_to_watch));
// Paths to stop watching are in the current set, but not new
std::set_difference(currently_monitored_paths_.begin(), currently_monitored_paths_.end(),
new_monitored_paths.begin(), new_monitored_paths.end(),
std::back_inserter(paths_to_stop_watching));
std::swap(watch_items_, new_tree);
std::swap(currently_monitored_paths_, new_monitored_paths);
current_config_ = new_config;
if (new_config) {
policy_version_ = [new_config[kWatchItemConfigKeyVersion] UTF8String];
} else {
policy_version_ = "";
}
last_update_time_ = [[NSDate date] timeIntervalSince1970];
LOGD(@"Changes to watch items detected, notifying registered clients.");
for (const id<SNTEndpointSecurityDynamicEventHandler> &client : registerd_clients_) {
// Note: Enable clients on an async queue in case they perform any
// synchronous work that could trigger ES events. Otherwise they might
// trigger AUTH ES events that would attempt to re-enter this object and
// potentially deadlock.
dispatch_async(q_, ^{
[client watchItemsCount:currently_monitored_paths_.size()
newPaths:paths_to_watch
removedPaths:paths_to_stop_watching];
});
}
} else {
LOGD(@"No changes to set of watched paths.");
}
}
void WatchItems::ReloadConfig(NSDictionary *new_config) {
std::vector<std::shared_ptr<WatchItemPolicy>> new_policies;
auto new_tree = std::make_unique<PrefixTree<std::shared_ptr<WatchItemPolicy>>>();
std::set<std::pair<std::string, WatchItemPathType>> new_monitored_paths;
if (new_config) {
NSError *err;
if (!ParseConfig(new_config, new_policies, &err)) {
LOGE(@"Failed to parse watch item config: %@",
err ? err.localizedDescription : @"Unknown failure");
return;
}
if (!BuildPolicyTree(new_policies, *new_tree, new_monitored_paths)) {
LOGE(@"Failed to build new filesystem monitoring policy");
return;
}
}
UpdateCurrentState(std::move(new_tree), std::move(new_monitored_paths), new_config);
}
NSDictionary *WatchItems::ReadConfig() {
absl::ReaderMutexLock lock(&lock_);
return ReadConfigLocked();
}
NSDictionary *WatchItems::ReadConfigLocked() {
if (config_path_) {
return [NSDictionary dictionaryWithContentsOfFile:config_path_];
} else {
return nil;
}
}
void WatchItems::BeginPeriodicTask() {
if (periodic_task_started_) {
return;
}
std::weak_ptr<WatchItems> weak_watcher = weak_from_this();
dispatch_source_set_event_handler(timer_source_, ^{
std::shared_ptr<WatchItems> shared_watcher = weak_watcher.lock();
if (!shared_watcher) {
return;
}
shared_watcher->ReloadConfig(embedded_config_ ?: shared_watcher->ReadConfig());
if (shared_watcher->periodic_task_complete_f_) {
shared_watcher->periodic_task_complete_f_();
}
});
dispatch_resume(timer_source_);
periodic_task_started_ = true;
}
WatchItems::VersionAndPolicies WatchItems::FindPolciesForPaths(
const std::vector<std::string_view> &paths) {
absl::ReaderMutexLock lock(&lock_);
std::vector<std::optional<std::shared_ptr<WatchItemPolicy>>> policies;
for (const auto &path : paths) {
policies.push_back(watch_items_->LookupLongestMatchingPrefix(path.data()));
}
return {policy_version_, policies};
}
void WatchItems::SetConfigPath(NSString *config_path) {
// Acquire the lock to set the config path and read the config, but drop
// the lock before reloading the config
NSDictionary *config;
{
absl::MutexLock lock(&lock_);
config_path_ = config_path;
embedded_config_ = nil;
config = ReadConfigLocked();
}
ReloadConfig(config);
}
void WatchItems::SetConfig(NSDictionary *config) {
{
absl::MutexLock lock(&lock_);
config_path_ = nil;
embedded_config_ = config;
}
ReloadConfig(embedded_config_);
}
std::optional<WatchItemsState> WatchItems::State() {
absl::ReaderMutexLock lock(&lock_);
if (!current_config_) {
return std::nullopt;
}
WatchItemsState state = {
.rule_count = [current_config_[kWatchItemConfigKeyWatchItems] count],
.policy_version = [NSString stringWithUTF8String:policy_version_.c_str()],
.config_path = [config_path_ copy],
.last_config_load_epoch = last_update_time_,
};
return state;
}
} // namespace santa::santad::data_layer

View File

@@ -0,0 +1,870 @@
/// Copyright 2022 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 <CommonCrypto/CommonDigest.h>
#import <Foundation/Foundation.h>
#include <Kernel/kern/cs_blobs.h>
#import <XCTest/XCTest.h>
#include <dispatch/dispatch.h>
#include <sys/syslimits.h>
#include <unistd.h>
#include <algorithm>
#include <iostream>
#include <map>
#include <memory>
#include <variant>
#include <vector>
#include "Source/common/TestUtils.h"
#import "Source/common/Unit.h"
#include "Source/santad/DataLayer/WatchItemPolicy.h"
#include "Source/santad/DataLayer/WatchItems.h"
using santa::common::Unit;
using santa::santad::data_layer::kWatchItemPolicyDefaultAllowReadAccess;
using santa::santad::data_layer::kWatchItemPolicyDefaultAuditOnly;
using santa::santad::data_layer::kWatchItemPolicyDefaultPathType;
using santa::santad::data_layer::WatchItemPathType;
using santa::santad::data_layer::WatchItemPolicy;
using santa::santad::data_layer::WatchItems;
using santa::santad::data_layer::WatchItemsState;
namespace santatest {
using PathAndTypePair = std::pair<std::string, WatchItemPathType>;
using PathList = std::vector<PathAndTypePair>;
using ProcessList = std::vector<WatchItemPolicy::Process>;
} // namespace santatest
namespace santa::santad::data_layer {
extern bool ParseConfig(NSDictionary *config,
std::vector<std::shared_ptr<WatchItemPolicy>> &policies, NSError **err);
extern bool IsWatchItemNameValid(NSString *watch_item_name, NSError **err);
extern bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
std::vector<std::shared_ptr<WatchItemPolicy>> &policies,
NSError **err);
extern std::variant<Unit, santatest::PathList> VerifyConfigWatchItemPaths(NSArray<id> *paths,
NSError **err);
extern std::variant<Unit, santatest::ProcessList> VerifyConfigWatchItemProcesses(
NSDictionary *watch_item, NSError **err);
class WatchItemsPeer : public WatchItems {
public:
using WatchItems::WatchItems;
using WatchItems::ReloadConfig;
using WatchItems::SetConfig;
using WatchItems::SetConfigPath;
using WatchItems::config_path_;
using WatchItems::embedded_config_;
};
} // namespace santa::santad::data_layer
using santa::santad::data_layer::IsWatchItemNameValid;
using santa::santad::data_layer::ParseConfig;
using santa::santad::data_layer::ParseConfigSingleWatchItem;
using santa::santad::data_layer::VerifyConfigWatchItemPaths;
using santa::santad::data_layer::VerifyConfigWatchItemProcesses;
using santa::santad::data_layer::WatchItemsPeer;
static constexpr std::string_view kBadPolicyName("__BAD_NAME__");
static constexpr std::string_view kBadPolicyPath("__BAD_PATH__");
static constexpr std::string_view kVersion("v0.1");
static std::shared_ptr<WatchItemPolicy> MakeBadPolicy() {
return std::make_shared<WatchItemPolicy>(kBadPolicyName, kBadPolicyPath);
}
static NSMutableDictionary *WrapWatchItemsConfig(NSDictionary *config) {
return [@{@"Version" : @(kVersion.data()), @"WatchItems" : [config mutableCopy]} mutableCopy];
}
static NSString *RepeatedString(NSString *str, NSUInteger len) {
return [@"" stringByPaddingToLength:len withString:str startingAtIndex:0];
}
@interface WatchItemsTest : XCTestCase
@property NSFileManager *fileMgr;
@property NSString *testDir;
@property NSMutableArray *dirStack;
@property dispatch_queue_t q;
@end
@implementation WatchItemsTest
- (void)setUp {
self.dirStack = [[NSMutableArray alloc] init];
self.fileMgr = [NSFileManager defaultManager];
self.testDir =
[NSString stringWithFormat:@"%@santa-watchitems-%d", NSTemporaryDirectory(), getpid()];
XCTAssertTrue([self.fileMgr createDirectoryAtPath:self.testDir
withIntermediateDirectories:YES
attributes:nil
error:nil]);
self.q = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
XCTAssertNotNil(self.q);
}
- (void)tearDown {
XCTAssertTrue([self.fileMgr removeItemAtPath:self.testDir error:nil]);
}
- (void)pushd:(NSString *)path withRoot:(NSString *)root {
NSString *dir = [NSString pathWithComponents:@[ root, path ]];
NSString *origCwd = [self.fileMgr currentDirectoryPath];
XCTAssertNotNil(origCwd);
XCTAssertTrue([self.fileMgr changeCurrentDirectoryPath:dir]);
[self.dirStack addObject:origCwd];
}
- (void)pushd:(NSString *)dir {
[self pushd:dir withRoot:self.testDir];
}
- (void)popd {
NSString *dir = [self.dirStack lastObject];
XCTAssertTrue([self.fileMgr changeCurrentDirectoryPath:dir]);
[self.dirStack removeLastObject];
}
- (void)createTestDirStructure:(NSArray *)fs rootedAt:(NSString *)root {
NSString *origCwd = [self.fileMgr currentDirectoryPath];
XCTAssertNotNil(origCwd);
XCTAssertTrue([self.fileMgr changeCurrentDirectoryPath:root]);
for (id item in fs) {
if ([item isKindOfClass:[NSString class]]) {
XCTAssertTrue([self.fileMgr createFileAtPath:item contents:nil attributes:nil]);
} else if ([item isKindOfClass:[NSDictionary class]]) {
for (id dir in item) {
XCTAssertTrue([item[dir] isKindOfClass:[NSArray class]]);
XCTAssertTrue([self.fileMgr createDirectoryAtPath:dir
withIntermediateDirectories:NO
attributes:nil
error:nil]);
[self createTestDirStructure:item[dir] rootedAt:dir];
}
} else {
XCTFail("Unexpected dir structure item: %@: %@", item, [item class]);
}
}
XCTAssertTrue([self.fileMgr changeCurrentDirectoryPath:origCwd]);
}
- (void)createTestDirStructure:(NSArray *)fs {
[self createTestDirStructure:fs rootedAt:self.testDir];
}
- (void)testReloadScenarios {
[self createTestDirStructure:@[
@{
@"a" : @[ @"f1", @"f2" ],
},
@{
@"b" : @[ @"f1" ],
},
]];
NSDictionary *allFilesPolicy = @{kWatchItemConfigKeyPaths : @[ @"*" ]};
NSDictionary *configAllFilesOriginal =
WrapWatchItemsConfig(@{@"all_files_orig" : allFilesPolicy});
NSDictionary *configAllFilesRename =
WrapWatchItemsConfig(@{@"all_files_rename" : allFilesPolicy});
WatchItems::VersionAndPolicies policies;
std::vector<std::string_view> f1Path = {"f1"};
std::vector<std::string_view> f2Path = {"f2"};
// Changes in config dictionary will update policy info even if the
// filesystem didn't change.
{
WatchItemsPeer watchItems((NSString *)nil, NULL, NULL);
[self pushd:@"a"];
watchItems.ReloadConfig(configAllFilesOriginal);
policies = watchItems.FindPolciesForPaths(f1Path);
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(),
"all_files_orig");
watchItems.ReloadConfig(configAllFilesRename);
policies = watchItems.FindPolciesForPaths(f1Path);
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(),
"all_files_rename");
policies = watchItems.FindPolciesForPaths(f1Path);
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(),
"all_files_rename");
[self popd];
}
// Changes to fileystem structure are reflected when a config is reloaded
{
WatchItemsPeer watchItems((NSString *)nil, NULL, NULL);
[self pushd:@"a"];
watchItems.ReloadConfig(configAllFilesOriginal);
[self popd];
policies = watchItems.FindPolciesForPaths(f2Path);
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(),
"all_files_orig");
[self pushd:@"b"];
watchItems.ReloadConfig(configAllFilesOriginal);
[self popd];
policies = watchItems.FindPolciesForPaths(f2Path);
XCTAssertFalse(policies.second[0].has_value());
}
}
- (void)testPeriodicTask {
// Ensure watch item policy memory is properly handled
[self createTestDirStructure:@[ @"f1", @"f2", @"weird1" ]];
NSDictionary *fFiles = @{
kWatchItemConfigKeyPaths : @[ @{
kWatchItemConfigKeyPathsPath : @"f?",
kWatchItemConfigKeyPathsIsPrefix : @(NO),
} ]
};
NSDictionary *weirdFiles = @{
kWatchItemConfigKeyPaths : @[ @{
kWatchItemConfigKeyPathsPath : @"weird?",
kWatchItemConfigKeyPathsIsPrefix : @(NO),
} ]
};
NSString *configFile = @"config.plist";
NSDictionary *firstConfig = WrapWatchItemsConfig(@{@"f_files" : fFiles});
NSDictionary *secondConfig =
WrapWatchItemsConfig(@{@"f_files" : fFiles, @"weird_files" : weirdFiles});
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
const uint64 periodicFlushMS = 1000;
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 0),
NSEC_PER_MSEC * periodicFlushMS, 0);
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
auto watchItems = std::make_shared<WatchItemsPeer>(configFile, self.q, timer, ^{
dispatch_semaphore_signal(sema);
});
// Move into the base test directory and write the config to disk
[self pushd:@""];
XCTAssertTrue([firstConfig writeToFile:configFile atomically:YES]);
std::vector<std::string_view> f1Path = {"f1"};
std::vector<std::string_view> weird1Path = {"weird1"};
// Ensure no policy has been loaded yet
XCTAssertFalse(watchItems->FindPolciesForPaths(f1Path).second[0].has_value());
XCTAssertFalse(watchItems->FindPolciesForPaths(weird1Path).second[0].has_value());
// Begin the periodic task
watchItems->BeginPeriodicTask();
// The first run of the task starts immediately
// Wait for the first iteration and check for the expected policy
XCTAssertSemaTrue(sema, 5, "Periodic task did not complete within expected window");
XCTAssertTrue(watchItems->FindPolciesForPaths(f1Path).second[0].has_value());
XCTAssertFalse(watchItems->FindPolciesForPaths(weird1Path).second[0].has_value());
// Write the config update
XCTAssertTrue([secondConfig writeToFile:configFile atomically:YES]);
// Wait for the new config to be loaded and check for the new expected policies
XCTAssertSemaTrue(sema, 5, "Periodic task did not complete within expected window");
XCTAssertTrue(watchItems->FindPolciesForPaths(f1Path).second[0].has_value());
XCTAssertTrue(watchItems->FindPolciesForPaths(weird1Path).second[0].has_value());
[self popd];
}
- (void)testPolicyLookup {
// Test multiple, more comprehensive policies before/after config reload
[self createTestDirStructure:@[
@{
@"foo" : @[ @"bar.txt", @"bar.txt.tmp" ],
@"baz" : @[ @{@"qaz" : @[]} ],
},
@"f1",
]];
NSMutableDictionary *config = WrapWatchItemsConfig(@{
@"foo_subdir" : @{
kWatchItemConfigKeyPaths : @[ @{
kWatchItemConfigKeyPathsPath : @"./foo",
kWatchItemConfigKeyPathsIsPrefix : @(YES),
} ]
}
});
WatchItemsPeer watchItems((NSString *)nil, NULL, NULL);
WatchItems::VersionAndPolicies policies;
// Resultant vector is same size as input vector
// Initially nothing should be in the map
std::vector<std::string_view> paths = {};
XCTAssertEqual(watchItems.FindPolciesForPaths(paths).second.size(), 0);
paths.push_back("./foo");
XCTAssertEqual(watchItems.FindPolciesForPaths(paths).second.size(), 1);
XCTAssertFalse(watchItems.FindPolciesForPaths(paths).second[0].has_value());
paths.push_back("./baz");
XCTAssertEqual(watchItems.FindPolciesForPaths(paths).second.size(), 2);
// Load the initial config
[self pushd:@""];
watchItems.ReloadConfig(config);
[self popd];
{
// Test expected values with the inital policy
const std::map<std::vector<std::string_view>, std::string_view> pathToPolicyName = {
{{"./foo"}, "foo_subdir"},
{{"./foo/bar.txt.tmp"}, "foo_subdir"},
{{"./foo/bar.txt"}, "foo_subdir"},
{{"./does/not/exist"}, kBadPolicyName},
};
for (const auto &kv : pathToPolicyName) {
policies = watchItems.FindPolciesForPaths(kv.first);
XCTAssertCStringEqual(policies.first.data(), kVersion.data());
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(),
kv.second.data());
}
// Test multiple lookup
policies = watchItems.FindPolciesForPaths({"./foo", "./does/not/exist"});
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(), "foo_subdir");
XCTAssertFalse(policies.second[1].has_value());
}
// Add a new policy and reload the config
NSDictionary *barTxtFilePolicy = @{
kWatchItemConfigKeyPaths : @[ @{
kWatchItemConfigKeyPathsPath : @"./foo/bar.txt",
kWatchItemConfigKeyPathsIsPrefix : @(NO),
} ]
};
[config[@"WatchItems"] setObject:barTxtFilePolicy forKey:@"bar_txt"];
// Load the updated config
[self pushd:@""];
watchItems.ReloadConfig(config);
[self popd];
{
// Test expected values with the updated policy
const std::map<std::vector<std::string_view>, std::string_view> pathToPolicyName = {
{{"./foo"}, "foo_subdir"},
{{"./foo/bar.txt.tmp"}, "foo_subdir"},
{{"./foo/bar.txt"}, "bar_txt"},
{{"./does/not/exist"}, kBadPolicyName},
};
for (const auto &kv : pathToPolicyName) {
policies = watchItems.FindPolciesForPaths(kv.first);
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(),
kv.second.data());
}
}
// Add a catch-all policy that should only affect the previously non-matching path
NSDictionary *catchAllFilePolicy = @{
kWatchItemConfigKeyPaths : @[ @{
kWatchItemConfigKeyPathsPath : @".",
kWatchItemConfigKeyPathsIsPrefix : @(YES),
} ]
};
[config[@"WatchItems"] setObject:catchAllFilePolicy forKey:@"dot_everything"];
// Load the updated config
[self pushd:@""];
watchItems.ReloadConfig(config);
[self popd];
{
// Test expected values with the catch-all policy
const std::map<std::vector<std::string_view>, std::string_view> pathToPolicyName = {
{{"./foo"}, "foo_subdir"},
{{"./foo/bar.txt.tmp"}, "foo_subdir"},
{{"./foo/bar.txt"}, "bar_txt"},
{{"./does/not/exist"}, "dot_everything"},
};
for (const auto &kv : pathToPolicyName) {
policies = watchItems.FindPolciesForPaths(kv.first);
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(),
kv.second.data());
}
}
// Now remove the foo_subdir rule, previous matches should fallback to the catch-all
[config[@"WatchItems"] removeObjectForKey:@"foo_subdir"];
[self pushd:@""];
watchItems.ReloadConfig(config);
[self popd];
{
// Test expected values with the foo_subdir policy removed
const std::map<std::vector<std::string_view>, std::string_view> pathToPolicyName = {
{{"./foo"}, "dot_everything"},
{{"./foo/bar.txt.tmp"}, "dot_everything"},
{{"./foo/bar.txt"}, "bar_txt"},
{{"./does/not/exist"}, "dot_everything"},
};
for (const auto &kv : pathToPolicyName) {
policies = watchItems.FindPolciesForPaths(kv.first);
XCTAssertCStringEqual(policies.second[0].value_or(MakeBadPolicy())->name.c_str(),
kv.second.data());
}
}
}
- (void)testVerifyConfigWatchItemPaths {
std::variant<Unit, santatest::PathList> path_list;
NSError *err;
// Test no paths specified
path_list = VerifyConfigWatchItemPaths(@[], &err);
XCTAssertTrue(std::holds_alternative<Unit>(path_list));
// Test invalid types in paths array
path_list = VerifyConfigWatchItemPaths(@[ @(0) ], &err);
XCTAssertTrue(std::holds_alternative<Unit>(path_list));
// Test path array with long string
path_list = VerifyConfigWatchItemPaths(@[ RepeatedString(@"A", PATH_MAX + 1) ], &err);
XCTAssertTrue(std::holds_alternative<Unit>(path_list));
// Test path array dictionary with missing required key
path_list = VerifyConfigWatchItemPaths(@[ @{@"FakePath" : @"A"} ], &err);
XCTAssertTrue(std::holds_alternative<Unit>(path_list));
// Test path array dictionary with long string
path_list = VerifyConfigWatchItemPaths(
@[ @{kWatchItemConfigKeyPathsPath : RepeatedString(@"A", PATH_MAX + 1)} ], &err);
XCTAssertTrue(std::holds_alternative<Unit>(path_list));
// Test path array dictionary with default path type
path_list = VerifyConfigWatchItemPaths(@[ @{kWatchItemConfigKeyPathsPath : @"A"} ], &err);
XCTAssertTrue(std::holds_alternative<santatest::PathList>(path_list));
XCTAssertEqual(std::get<santatest::PathList>(path_list).size(), 1);
XCTAssertCStringEqual(std::get<santatest::PathList>(path_list)[0].first.c_str(), "A");
XCTAssertEqual(std::get<santatest::PathList>(path_list)[0].second,
kWatchItemPolicyDefaultPathType);
// Test path array dictionary with custom path type
path_list = VerifyConfigWatchItemPaths(
@[ @{kWatchItemConfigKeyPathsPath : @"A", kWatchItemConfigKeyPathsIsPrefix : @(YES)} ], &err);
XCTAssertTrue(std::holds_alternative<santatest::PathList>(path_list));
XCTAssertEqual(std::get<santatest::PathList>(path_list).size(), 1);
XCTAssertCStringEqual(std::get<santatest::PathList>(path_list)[0].first.c_str(), "A");
XCTAssertEqual(std::get<santatest::PathList>(path_list)[0].second, WatchItemPathType::kPrefix);
}
- (void)testVerifyConfigWatchItemProcesses {
std::variant<Unit, santatest::ProcessList> proc_list;
NSError *err;
// Non-existent process list parses successfully, but has no items
proc_list = VerifyConfigWatchItemProcesses(@{}, &err);
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 0);
// Process list fails to parse if contains non-array type
proc_list = VerifyConfigWatchItemProcesses(@{kWatchItemConfigKeyProcesses : @""}, &err);
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
proc_list = VerifyConfigWatchItemProcesses(@{kWatchItemConfigKeyProcesses : @(0)}, &err);
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
proc_list = VerifyConfigWatchItemProcesses(@{kWatchItemConfigKeyProcesses : @{}}, &err);
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
proc_list = VerifyConfigWatchItemProcesses(@{kWatchItemConfigKeyProcesses : @[]}, &err);
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
// Test a process dictionary with no valid attributes set
proc_list = VerifyConfigWatchItemProcesses(@{kWatchItemConfigKeyProcesses : @[ @{} ]}, &err);
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
// Test BinaryPath length limits
proc_list = VerifyConfigWatchItemProcesses(@{
kWatchItemConfigKeyProcesses :
@[ @{kWatchItemConfigKeyProcessesBinaryPath : RepeatedString(@"A", PATH_MAX + 1)} ]
},
&err);
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
// Test valid BinaryPath
proc_list = VerifyConfigWatchItemProcesses(
@{kWatchItemConfigKeyProcesses : @[ @{kWatchItemConfigKeyProcessesBinaryPath : @"mypath"} ]},
&err);
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
WatchItemPolicy::Process("mypath", "", "", {}, "", std::nullopt));
// Test SigningID length limits
proc_list = VerifyConfigWatchItemProcesses(@{
kWatchItemConfigKeyProcesses :
@[ @{kWatchItemConfigKeyProcessesSigningID : RepeatedString(@"A", 513)} ]
},
&err);
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
// Test valid SigningID
proc_list = VerifyConfigWatchItemProcesses(@{
kWatchItemConfigKeyProcesses :
@[ @{kWatchItemConfigKeyProcessesSigningID : @"com.google.test"} ]
},
&err);
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
WatchItemPolicy::Process("", "com.google.test", "", {}, "", std::nullopt));
// Test TeamID length limits
proc_list = VerifyConfigWatchItemProcesses(@{
kWatchItemConfigKeyProcesses :
@[ @{kWatchItemConfigKeyProcessesTeamID : @"LongerThanExpectedTeamID"} ]
},
&err);
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
// Test valid TeamID
proc_list = VerifyConfigWatchItemProcesses(
@{kWatchItemConfigKeyProcesses : @[ @{kWatchItemConfigKeyProcessesTeamID : @"myvalidtid"} ]},
&err);
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
WatchItemPolicy::Process("", "", "myvalidtid", {}, "", std::nullopt));
// Test CDHash length limits
proc_list = VerifyConfigWatchItemProcesses(@{
kWatchItemConfigKeyProcesses :
@[ @{kWatchItemConfigKeyProcessesCDHash : RepeatedString(@"A", CS_CDHASH_LEN * 2 + 1)} ]
},
&err);
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
// Test CDHash hex-only
proc_list = VerifyConfigWatchItemProcesses(@{
kWatchItemConfigKeyProcesses :
@[ @{kWatchItemConfigKeyProcessesCDHash : RepeatedString(@"Z", CS_CDHASH_LEN * 2)} ]
},
&err);
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
// Test valid CDHash
NSString *cdhash = RepeatedString(@"A", CS_CDHASH_LEN * 2);
std::vector<uint8_t> cdhashBytes(cdhash.length / 2);
std::fill(cdhashBytes.begin(), cdhashBytes.end(), 0xAA);
proc_list = VerifyConfigWatchItemProcesses(
@{kWatchItemConfigKeyProcesses : @[ @{kWatchItemConfigKeyProcessesCDHash : cdhash} ]}, &err);
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
WatchItemPolicy::Process("", "", "", cdhashBytes, "", std::nullopt));
// Test Cert Hash length limits
proc_list = VerifyConfigWatchItemProcesses(@{
kWatchItemConfigKeyProcesses : @[ @{
kWatchItemConfigKeyProcessesCertificateSha256 :
RepeatedString(@"A", CC_SHA256_DIGEST_LENGTH * 2 + 1)
} ]
},
&err);
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
// Test Cert Hash hex-only
proc_list = VerifyConfigWatchItemProcesses(@{
kWatchItemConfigKeyProcesses : @[ @{
kWatchItemConfigKeyProcessesCertificateSha256 :
RepeatedString(@"Z", CC_SHA256_DIGEST_LENGTH * 2)
} ]
},
&err);
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
// Test valid Cert Hash
NSString *certHash = RepeatedString(@"A", CC_SHA256_DIGEST_LENGTH * 2);
proc_list = VerifyConfigWatchItemProcesses(@{
kWatchItemConfigKeyProcesses : @[ @{kWatchItemConfigKeyProcessesCertificateSha256 : certHash} ]
},
&err);
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
WatchItemPolicy::Process("", "", "", {}, [certHash UTF8String], std::nullopt));
// Test valid invalid PlatformBinary type
proc_list = VerifyConfigWatchItemProcesses(
@{kWatchItemConfigKeyProcesses : @[ @{kWatchItemConfigKeyProcessesPlatformBinary : @"YES"} ]},
&err);
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
// Test valid valid PlatformBinary
proc_list = VerifyConfigWatchItemProcesses(
@{kWatchItemConfigKeyProcesses : @[ @{kWatchItemConfigKeyProcessesPlatformBinary : @(YES)} ]},
&err);
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
WatchItemPolicy::Process("", "", "", {}, "", std::make_optional(true)));
// Test valid multiple attributes, multiple procs
proc_list = VerifyConfigWatchItemProcesses(@{
kWatchItemConfigKeyProcesses : @[
@{
kWatchItemConfigKeyProcessesBinaryPath : @"mypath1",
kWatchItemConfigKeyProcessesSigningID : @"com.google.test1",
kWatchItemConfigKeyProcessesTeamID : @"validtid_1",
kWatchItemConfigKeyProcessesCDHash : cdhash,
kWatchItemConfigKeyProcessesCertificateSha256 : certHash,
kWatchItemConfigKeyProcessesPlatformBinary : @(YES),
},
@{
kWatchItemConfigKeyProcessesBinaryPath : @"mypath2",
kWatchItemConfigKeyProcessesSigningID : @"com.google.test2",
kWatchItemConfigKeyProcessesTeamID : @"validtid_2",
kWatchItemConfigKeyProcessesCDHash : cdhash,
kWatchItemConfigKeyProcessesCertificateSha256 : certHash,
kWatchItemConfigKeyProcessesPlatformBinary : @(NO),
},
]
},
&err);
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 2);
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
WatchItemPolicy::Process("mypath1", "com.google.test1", "validtid_1", cdhashBytes,
[certHash UTF8String], std::make_optional(true)));
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[1],
WatchItemPolicy::Process("mypath2", "com.google.test2", "validtid_2", cdhashBytes,
[certHash UTF8String], std::make_optional(false)));
}
- (void)testIsWatchItemNameValid {
// Only legal C identifiers should be accepted
XCTAssertFalse(IsWatchItemNameValid(nil, nil));
XCTAssertFalse(IsWatchItemNameValid(@"", nil));
XCTAssertFalse(IsWatchItemNameValid(@"1abc", nil));
XCTAssertFalse(IsWatchItemNameValid(@"abc-1234", nil));
XCTAssertFalse(IsWatchItemNameValid(@"a=b", nil));
XCTAssertFalse(IsWatchItemNameValid(@"a!b", nil));
XCTAssertTrue(IsWatchItemNameValid(@"_", nil));
XCTAssertTrue(IsWatchItemNameValid(@"_1", nil));
XCTAssertTrue(IsWatchItemNameValid(@"_1_", nil));
XCTAssertTrue(IsWatchItemNameValid(@"abc", nil));
XCTAssertTrue(IsWatchItemNameValid(@"A", nil));
XCTAssertTrue(IsWatchItemNameValid(@"A_B", nil));
XCTAssertTrue(IsWatchItemNameValid(@"FooName", nil));
XCTAssertTrue(IsWatchItemNameValid(@"bar_Name", nil));
}
- (void)testParseConfig {
NSError *err;
std::vector<std::shared_ptr<WatchItemPolicy>> policies;
// Ensure top level keys must exist and be correct types
XCTAssertFalse(ParseConfig(@{}, policies, &err));
XCTAssertFalse(ParseConfig(@{kWatchItemConfigKeyVersion : @(0)}, policies, &err));
XCTAssertFalse(ParseConfig(@{kWatchItemConfigKeyVersion : @{}}, policies, &err));
XCTAssertFalse(ParseConfig(@{kWatchItemConfigKeyVersion : @[]}, policies, &err));
XCTAssertFalse(ParseConfig(@{kWatchItemConfigKeyVersion : @""}, policies, &err));
XCTAssertFalse(ParseConfig(
@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @""}, policies, &err));
XCTAssertFalse(ParseConfig(
@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @[]}, policies, &err));
XCTAssertFalse(ParseConfig(
@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @(0)}, policies, &err));
// Minimally successful configs without watch items
XCTAssertTrue(ParseConfig(@{kWatchItemConfigKeyVersion : @"1"}, policies, &err));
XCTAssertTrue(ParseConfig(
@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{}}, policies, &err));
// Ensure constraints on watch items entries match expectations
XCTAssertFalse(ParseConfig(
@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@(0) : @(0)}}, policies,
&err));
XCTAssertFalse(
ParseConfig(@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@"" : @{}}},
policies, &err));
XCTAssertFalse(
ParseConfig(@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@"a" : @[]}},
policies, &err));
XCTAssertFalse(
ParseConfig(@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@"a" : @{}}},
policies, &err));
// Minimally successful config with watch item
XCTAssertTrue(ParseConfig(@{
kWatchItemConfigKeyVersion : @"1",
kWatchItemConfigKeyWatchItems : @{@"a" : @{kWatchItemConfigKeyPaths : @[ @"asdf" ]}}
},
policies, &err));
}
- (void)testParseConfigSingleWatchItem {
std::vector<std::shared_ptr<WatchItemPolicy>> policies;
NSError *err;
// There must be valid Paths in a watch item
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{}, policies, &err));
XCTAssertFalse(
ParseConfigSingleWatchItem(@"", @{kWatchItemConfigKeyPaths : @[ @"" ]}, policies, &err));
XCTAssertTrue(
ParseConfigSingleWatchItem(@"", @{kWatchItemConfigKeyPaths : @[ @"a" ]}, policies, &err));
// Empty options are fine
XCTAssertTrue(ParseConfigSingleWatchItem(
@"", @{kWatchItemConfigKeyPaths : @[ @"a" ], kWatchItemConfigKeyOptions : @{}}, policies,
&err));
// If an Options key exist, it must be a dictionary type
XCTAssertFalse(ParseConfigSingleWatchItem(
@"", @{kWatchItemConfigKeyPaths : @[ @"a" ], kWatchItemConfigKeyOptions : @[]}, policies,
&err));
XCTAssertFalse(ParseConfigSingleWatchItem(
@"", @{kWatchItemConfigKeyPaths : @[ @"a" ], kWatchItemConfigKeyOptions : @""}, policies,
&err));
XCTAssertFalse(ParseConfigSingleWatchItem(
@"", @{kWatchItemConfigKeyPaths : @[ @"a" ], kWatchItemConfigKeyOptions : @(0)}, policies,
&err));
// Options keys must be valid types
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAllowReadAccess : @""}
},
policies, &err));
XCTAssertTrue(ParseConfigSingleWatchItem(@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAllowReadAccess : @(0)}
},
policies, &err));
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAuditOnly : @""}
},
policies, &err));
XCTAssertTrue(ParseConfigSingleWatchItem(@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAuditOnly : @(0)}
},
policies, &err));
// If processes are specified, they must be valid format
// Note: Full tests in `testVerifyConfigWatchItemProcesses`
XCTAssertFalse(ParseConfigSingleWatchItem(
@"", @{kWatchItemConfigKeyPaths : @[ @"a" ], kWatchItemConfigKeyProcesses : @""}, policies,
&err));
// Test the policy vector is populated as expected
// Test default options with no processes
policies.clear();
XCTAssertTrue(
ParseConfigSingleWatchItem(@"rule", @{kWatchItemConfigKeyPaths : @[ @"a" ]}, policies, &err));
XCTAssertEqual(policies.size(), 1);
XCTAssertEqual(*policies[0].get(), WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType,
kWatchItemPolicyDefaultAllowReadAccess,
kWatchItemPolicyDefaultAuditOnly, {}));
// Test multiple paths, options, and processes
policies.clear();
std::vector<WatchItemPolicy::Process> procs = {
WatchItemPolicy::Process("pa", "", "", {}, "", std::nullopt),
WatchItemPolicy::Process("pb", "", "", {}, "", std::nullopt),
};
XCTAssertTrue(ParseConfigSingleWatchItem(@"rule", @{
kWatchItemConfigKeyPaths :
@[ @"a", @{kWatchItemConfigKeyPathsPath : @"b", kWatchItemConfigKeyPathsIsPrefix : @(YES)} ],
kWatchItemConfigKeyOptions : @{
kWatchItemConfigKeyOptionsAllowReadAccess : @(YES),
kWatchItemConfigKeyOptionsAuditOnly : @(NO)
},
kWatchItemConfigKeyProcesses : @[
@{kWatchItemConfigKeyProcessesBinaryPath : @"pa"},
@{kWatchItemConfigKeyProcessesBinaryPath : @"pb"}
]
},
policies, &err));
XCTAssertEqual(policies.size(), 2);
XCTAssertEqual(*policies[0].get(),
WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType, true, false, procs));
XCTAssertEqual(*policies[1].get(),
WatchItemPolicy("rule", "b", WatchItemPathType::kPrefix, true, false, procs));
}
- (void)testState {
NSString *configPath = @"my_config_path";
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
NSMutableDictionary *config = WrapWatchItemsConfig(@{
@"rule1" : @{kWatchItemConfigKeyPaths : @[ @"abc" ]},
@"rule2" : @{kWatchItemConfigKeyPaths : @[ @"xyz" ]}
});
WatchItemsPeer watchItems(configPath, NULL, NULL);
// If no policy yet exists, nullopt is returned
std::optional<WatchItemsState> optionalState = watchItems.State();
XCTAssertFalse(optionalState.has_value());
watchItems.ReloadConfig(config);
optionalState = watchItems.State();
XCTAssertTrue(optionalState.has_value());
WatchItemsState state = optionalState.value();
XCTAssertEqual(state.rule_count, [config[kWatchItemConfigKeyWatchItems] count]);
XCTAssertCStringEqual(state.policy_version.UTF8String, kVersion.data());
XCTAssertEqual(state.config_path, configPath);
XCTAssertGreaterThanOrEqual(state.last_config_load_epoch, startTime);
}
- (void)testSetConfigAndSetConfigPath {
// Test internal state when switching back and forth between path-based and
// dictionary-based config options.
WatchItemsPeer watchItems(@{}, NULL, NULL);
XCTAssertNil(watchItems.config_path_);
XCTAssertNotNil(watchItems.embedded_config_);
watchItems.SetConfigPath(@"/path/to/a/nonexistent/file/so/nothing/is/opened");
XCTAssertNotNil(watchItems.config_path_);
XCTAssertNil(watchItems.embedded_config_);
watchItems.SetConfig(@{});
XCTAssertNil(watchItems.config_path_);
XCTAssertNotNil(watchItems.embedded_config_);
}
@end

View File

@@ -21,8 +21,9 @@
#include <sys/stat.h>
#include <memory>
#import "Source/common/SNTCommon.h"
#import "Source/common/SNTCommonEnums.h"
#include "Source/common/SantaCache.h"
#import "Source/common/SantaVnode.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
namespace santa::santad::event_providers {
@@ -49,20 +50,20 @@ class AuthResultCache {
AuthResultCache(const AuthResultCache &other) = delete;
AuthResultCache &operator=(const AuthResultCache &other) = delete;
virtual bool AddToCache(const es_file_t *es_file, santa_action_t decision);
virtual bool AddToCache(const es_file_t *es_file, SNTAction decision);
virtual void RemoveFromCache(const es_file_t *es_file);
virtual santa_action_t CheckCache(const es_file_t *es_file);
virtual santa_action_t CheckCache(santa_vnode_id_t vnode_id);
virtual SNTAction CheckCache(const es_file_t *es_file);
virtual SNTAction CheckCache(SantaVnode vnode_id);
virtual void FlushCache(FlushCacheMode mode);
virtual NSArray<NSNumber *> *CacheCounts();
private:
virtual SantaCache<santa_vnode_id_t, uint64_t> *CacheForVnodeID(santa_vnode_id_t vnode_id);
virtual SantaCache<SantaVnode, uint64_t> *CacheForVnodeID(SantaVnode vnode_id);
SantaCache<santa_vnode_id_t, uint64_t> *root_cache_;
SantaCache<santa_vnode_id_t, uint64_t> *nonroot_cache_;
SantaCache<SantaVnode, uint64_t> *root_cache_;
SantaCache<SantaVnode, uint64_t> *nonroot_cache_;
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
uint64_t root_devno_;

View File

@@ -19,37 +19,25 @@
#include <time.h>
#import "Source/common/SNTLogging.h"
#import "Source/common/SantaVnodeHash.h"
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
using santa::santad::event_providers::endpoint_security::Client;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
template <>
uint64_t SantaCacheHasher<santa_vnode_id_t>(santa_vnode_id_t const &t) {
return (SantaCacheHasher<uint64_t>(t.fsid) << 1) ^ SantaCacheHasher<uint64_t>(t.fileid);
}
namespace santa::santad::event_providers {
static inline santa_vnode_id_t VnodeForFile(const es_file_t *es_file) {
return santa_vnode_id_t{
.fsid = (uint64_t)es_file->stat.st_dev,
.fileid = es_file->stat.st_ino,
};
}
static inline uint64_t GetCurrentUptime() {
return clock_gettime_nsec_np(CLOCK_MONOTONIC);
}
// Decision is stored in upper 8 bits, timestamp in remaining 56.
static inline uint64_t CacheableAction(santa_action_t action,
uint64_t timestamp = GetCurrentUptime()) {
static inline uint64_t CacheableAction(SNTAction action, uint64_t timestamp = GetCurrentUptime()) {
return ((uint64_t)action << 56) | (timestamp & 0xFFFFFFFFFFFFFF);
}
static inline santa_action_t ActionFromCachedValue(uint64_t cachedValue) {
return (santa_action_t)(cachedValue >> 56);
static inline SNTAction ActionFromCachedValue(uint64_t cachedValue) {
return (SNTAction)(cachedValue >> 56);
}
static inline uint64_t TimestampFromCachedValue(uint64_t cachedValue) {
@@ -59,8 +47,8 @@ static inline uint64_t TimestampFromCachedValue(uint64_t cachedValue) {
AuthResultCache::AuthResultCache(std::shared_ptr<EndpointSecurityAPI> esapi,
uint64_t cache_deny_time_ms)
: esapi_(esapi), cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
root_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>();
nonroot_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>();
root_cache_ = new SantaCache<SantaVnode, uint64_t>();
nonroot_cache_ = new SantaCache<SantaVnode, uint64_t>();
struct stat sb;
if (stat("/", &sb) == 0) {
@@ -78,17 +66,17 @@ AuthResultCache::~AuthResultCache() {
delete nonroot_cache_;
}
bool AuthResultCache::AddToCache(const es_file_t *es_file, santa_action_t decision) {
santa_vnode_id_t vnode_id = VnodeForFile(es_file);
SantaCache<santa_vnode_id_t, uint64_t> *cache = CacheForVnodeID(vnode_id);
bool AuthResultCache::AddToCache(const es_file_t *es_file, SNTAction decision) {
SantaVnode vnode_id = SantaVnode::VnodeForFile(es_file);
SantaCache<SantaVnode, uint64_t> *cache = CacheForVnodeID(vnode_id);
switch (decision) {
case ACTION_REQUEST_BINARY:
return cache->set(vnode_id, CacheableAction(ACTION_REQUEST_BINARY, 0), 0);
case ACTION_RESPOND_ALLOW: OS_FALLTHROUGH;
case ACTION_RESPOND_ALLOW_COMPILER: OS_FALLTHROUGH;
case ACTION_RESPOND_DENY:
case SNTActionRequestBinary:
return cache->set(vnode_id, CacheableAction(SNTActionRequestBinary, 0), 0);
case SNTActionRespondAllow: OS_FALLTHROUGH;
case SNTActionRespondAllowCompiler: OS_FALLTHROUGH;
case SNTActionRespondDeny:
return cache->set(vnode_id, CacheableAction(decision),
CacheableAction(ACTION_REQUEST_BINARY, 0));
CacheableAction(SNTActionRequestBinary, 0));
default:
// This is a programming error. Bail.
LOGE(@"Invalid cache value, exiting.");
@@ -97,37 +85,36 @@ bool AuthResultCache::AddToCache(const es_file_t *es_file, santa_action_t decisi
}
void AuthResultCache::RemoveFromCache(const es_file_t *es_file) {
santa_vnode_id_t vnode_id = VnodeForFile(es_file);
SantaVnode vnode_id = SantaVnode::VnodeForFile(es_file);
CacheForVnodeID(vnode_id)->remove(vnode_id);
}
santa_action_t AuthResultCache::CheckCache(const es_file_t *es_file) {
return CheckCache(VnodeForFile(es_file));
SNTAction AuthResultCache::CheckCache(const es_file_t *es_file) {
return CheckCache(SantaVnode::VnodeForFile(es_file));
}
santa_action_t AuthResultCache::CheckCache(santa_vnode_id_t vnode_id) {
SantaCache<santa_vnode_id_t, uint64_t> *cache = CacheForVnodeID(vnode_id);
SNTAction AuthResultCache::CheckCache(SantaVnode vnode_id) {
SantaCache<SantaVnode, uint64_t> *cache = CacheForVnodeID(vnode_id);
uint64_t cached_val = cache->get(vnode_id);
if (cached_val == 0) {
return ACTION_UNSET;
return SNTActionUnset;
}
santa_action_t result = ActionFromCachedValue(cached_val);
SNTAction result = ActionFromCachedValue(cached_val);
if (result == ACTION_RESPOND_DENY) {
if (result == SNTActionRespondDeny) {
uint64_t expiry_time = TimestampFromCachedValue(cached_val) + cache_deny_time_ns_;
if (expiry_time < GetCurrentUptime()) {
cache->remove(vnode_id);
return ACTION_UNSET;
return SNTActionUnset;
}
}
return result;
}
SantaCache<santa_vnode_id_t, uint64_t> *AuthResultCache::CacheForVnodeID(
santa_vnode_id_t vnode_id) {
SantaCache<SantaVnode, uint64_t> *AuthResultCache::CacheForVnodeID(SantaVnode vnode_id) {
return (vnode_id.fsid == root_devno_ || root_devno_ == 0) ? root_cache_ : nonroot_cache_;
}

View File

@@ -22,7 +22,7 @@
#include <memory>
#include "Source/common/SNTCommon.h"
#include "Source/common/SantaVnode.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/EventProviders/AuthResultCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
@@ -47,13 +47,6 @@ static inline es_file_t MakeCacheableFile(uint64_t devno, uint64_t ino) {
.path = {}, .path_truncated = false, .stat = {.st_dev = (dev_t)devno, .st_ino = ino}};
}
static inline santa_vnode_id_t VnodeForFile(const es_file_t *es_file) {
return santa_vnode_id_t{
.fsid = (uint64_t)es_file->stat.st_dev,
.fileid = es_file->stat.st_ino,
};
}
static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uint64_t root_count,
uint64_t nonroot_count) {
NSArray<NSNumber *> *counts = cache->CacheCounts();
@@ -86,33 +79,33 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 222);
// Add the root file to the cache
cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY);
cache->AddToCache(&rootFile, SNTActionRequestBinary);
AssertCacheCounts(cache, 1, 0);
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_UNSET);
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionRequestBinary);
XCTAssertEqual(cache->CheckCache(&nonrootFile), SNTActionUnset);
// Now add the non-root file
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
cache->AddToCache(&nonrootFile, SNTActionRequestBinary);
AssertCacheCounts(cache, 1, 1);
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_REQUEST_BINARY);
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionRequestBinary);
XCTAssertEqual(cache->CheckCache(&nonrootFile), SNTActionRequestBinary);
// Update the cached values
cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW);
cache->AddToCache(&nonrootFile, ACTION_RESPOND_DENY);
cache->AddToCache(&rootFile, SNTActionRespondAllow);
cache->AddToCache(&nonrootFile, SNTActionRespondDeny);
AssertCacheCounts(cache, 1, 1);
XCTAssertEqual(cache->CheckCache(VnodeForFile(&rootFile)), ACTION_RESPOND_ALLOW);
XCTAssertEqual(cache->CheckCache(VnodeForFile(&nonrootFile)), ACTION_RESPOND_DENY);
XCTAssertEqual(cache->CheckCache(SantaVnode::VnodeForFile(&rootFile)), SNTActionRespondAllow);
XCTAssertEqual(cache->CheckCache(SantaVnode::VnodeForFile(&nonrootFile)), SNTActionRespondDeny);
// Remove the root file
cache->RemoveFromCache(&rootFile);
AssertCacheCounts(cache, 0, 1);
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_RESPOND_DENY);
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionUnset);
XCTAssertEqual(cache->CheckCache(&nonrootFile), SNTActionRespondDeny);
}
- (void)testFlushCache {
@@ -122,8 +115,8 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 111);
cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY);
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
cache->AddToCache(&rootFile, SNTActionRequestBinary);
cache->AddToCache(&nonrootFile, SNTActionRequestBinary);
AssertCacheCounts(cache, 1, 1);
@@ -133,7 +126,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
AssertCacheCounts(cache, 1, 0);
// Add back the non-root file
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
cache->AddToCache(&nonrootFile, SNTActionRequestBinary);
AssertCacheCounts(cache, 1, 1);
@@ -162,33 +155,33 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
// Cached items must first be in the ACTION_REQUEST_BINARY state
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW));
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW_COMPILER));
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_DENY));
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
// Cached items must first be in the SNTActionRequestBinary state
XCTAssertFalse(cache->AddToCache(&rootFile, SNTActionRespondAllow));
XCTAssertFalse(cache->AddToCache(&rootFile, SNTActionRespondAllowCompiler));
XCTAssertFalse(cache->AddToCache(&rootFile, SNTActionRespondDeny));
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionUnset);
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
XCTAssertTrue(cache->AddToCache(&rootFile, SNTActionRequestBinary));
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionRequestBinary);
// Items in the `ACTION_REQUEST_BINARY` state cannot reenter the same state
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
// Items in the `SNTActionRequestBinary` state cannot reenter the same state
XCTAssertFalse(cache->AddToCache(&rootFile, SNTActionRequestBinary));
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionRequestBinary);
santa_action_t allowed_transitions[] = {
ACTION_RESPOND_ALLOW,
ACTION_RESPOND_ALLOW_COMPILER,
ACTION_RESPOND_DENY,
SNTAction allowed_transitions[] = {
SNTActionRespondAllow,
SNTActionRespondAllowCompiler,
SNTActionRespondDeny,
};
for (size_t i = 0; i < sizeof(allowed_transitions) / sizeof(allowed_transitions[0]); i++) {
// First make sure the item doesn't exist
cache->RemoveFromCache(&rootFile);
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionUnset);
// Now add the item to be in the first allowed state
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
XCTAssertTrue(cache->AddToCache(&rootFile, SNTActionRequestBinary));
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionRequestBinary);
// Now assert the allowed transition
XCTAssertTrue(cache->AddToCache(&rootFile, allowed_transitions[i]));
@@ -204,12 +197,12 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
// Add a file to the cache and put into the ACTION_RESPOND_DENY state
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_RESPOND_DENY));
// Add a file to the cache and put into the SNTActionRespondDeny state
XCTAssertTrue(cache->AddToCache(&rootFile, SNTActionRequestBinary));
XCTAssertTrue(cache->AddToCache(&rootFile, SNTActionRespondDeny));
// Ensure the file exists
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_RESPOND_DENY);
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionRespondDeny);
// Wait for the item to expire
SleepMS(expiryMS);
@@ -218,7 +211,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
AssertCacheCounts(cache, 1, 0);
// Now check the cache, which will remove the item
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
XCTAssertEqual(cache->CheckCache(&rootFile), SNTActionUnset);
AssertCacheCounts(cache, 0, 0);
}

View File

@@ -19,7 +19,9 @@
#import <Foundation/Foundation.h>
#include <set>
#include <string_view>
#include "Source/santad/DataLayer/WatchItemPolicy.h"
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
@@ -32,12 +34,26 @@ class EndpointSecurityAPI : public std::enable_shared_from_this<EndpointSecurity
virtual Client NewClient(void (^message_handler)(es_client_t *, Message));
virtual bool Subscribe(const Client &client, const std::set<es_event_type_t> &);
virtual bool UnsubscribeAll(const Client &client);
virtual es_message_t *RetainMessage(const es_message_t *msg);
virtual void ReleaseMessage(es_message_t *msg);
virtual bool UnmuteAllPaths(const Client &client);
virtual bool UnmuteAllTargetPaths(const Client &client);
virtual bool IsTargetPathMutingInverted(const Client &client);
virtual bool InvertTargetPathMuting(const Client &client);
virtual bool MuteTargetPath(const Client &client, std::string_view path,
santa::santad::data_layer::WatchItemPathType path_type);
virtual bool UnmuteTargetPath(const Client &client, std::string_view path,
santa::santad::data_layer::WatchItemPathType path_type);
virtual void RetainMessage(const es_message_t *msg);
virtual void ReleaseMessage(const es_message_t *msg);
virtual bool RespondAuthResult(const Client &client, const Message &msg, es_auth_result_t result,
bool cache);
virtual bool RespondFlagsResult(const Client &client, const Message &msg, uint32_t allowed_flags,
bool cache);
virtual bool MuteProcess(const Client &client, const audit_token_t *tok);

View File

@@ -13,11 +13,14 @@
/// limitations under the License.
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include <EndpointSecurity/ESTypes.h>
#include <set>
#include <vector>
#include "Source/common/Platform.h"
using santa::santad::data_layer::WatchItemPathType;
namespace santa::santad::event_providers::endpoint_security {
Client EndpointSecurityAPI::NewClient(void (^message_handler)(es_client_t *, Message)) {
@@ -33,28 +36,12 @@ Client EndpointSecurityAPI::NewClient(void (^message_handler)(es_client_t *, Mes
return Client(client, res);
}
es_message_t *EndpointSecurityAPI::RetainMessage(const es_message_t *msg) {
if (@available(macOS 11.0, *)) {
es_retain_message(msg);
es_message_t *nonconst = const_cast<es_message_t *>(msg);
return nonconst;
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return es_copy_message(msg);
#pragma clang diagnostic pop
}
void EndpointSecurityAPI::RetainMessage(const es_message_t *msg) {
es_retain_message(msg);
}
void EndpointSecurityAPI::ReleaseMessage(es_message_t *msg) {
if (@available(macOS 11.0, *)) {
es_release_message(msg);
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return es_free_message(msg);
#pragma clang diagnostic pop
}
void EndpointSecurityAPI::ReleaseMessage(const es_message_t *msg) {
es_release_message(msg);
}
bool EndpointSecurityAPI::Subscribe(const Client &client,
@@ -63,11 +50,87 @@ bool EndpointSecurityAPI::Subscribe(const Client &client,
return es_subscribe(client.Get(), subs.data(), (uint32_t)subs.size()) == ES_RETURN_SUCCESS;
}
bool EndpointSecurityAPI::UnsubscribeAll(const Client &client) {
return es_unsubscribe_all(client.Get()) == ES_RETURN_SUCCESS;
}
bool EndpointSecurityAPI::UnmuteAllPaths(const Client &client) {
return es_unmute_all_paths(client.Get()) == ES_RETURN_SUCCESS;
}
bool EndpointSecurityAPI::UnmuteAllTargetPaths(const Client &client) {
#if HAVE_MACOS_13
if (@available(macOS 13.0, *)) {
return es_unmute_all_target_paths(client.Get()) == ES_RETURN_SUCCESS;
}
#endif
return true;
}
bool EndpointSecurityAPI::IsTargetPathMutingInverted(const Client &client) {
#if HAVE_MACOS_13
if (@available(macOS 13.0, *)) {
return es_muting_inverted(client.Get(), ES_MUTE_INVERSION_TYPE_TARGET_PATH) == ES_MUTE_INVERTED;
}
#endif
return false;
}
bool EndpointSecurityAPI::InvertTargetPathMuting(const Client &client) {
#if HAVE_MACOS_13
if (@available(macOS 13.0, *)) {
if (!IsTargetPathMutingInverted(client)) {
return es_invert_muting(client.Get(), ES_MUTE_INVERSION_TYPE_TARGET_PATH) ==
ES_RETURN_SUCCESS;
} else {
return true;
}
}
#endif
return false;
}
bool EndpointSecurityAPI::MuteTargetPath(const Client &client, std::string_view path,
WatchItemPathType path_type) {
#if HAVE_MACOS_13
if (@available(macOS 13.0, *)) {
return es_mute_path(client.Get(), path.data(),
path_type == WatchItemPathType::kPrefix
? ES_MUTE_PATH_TYPE_TARGET_PREFIX
: ES_MUTE_PATH_TYPE_TARGET_LITERAL) == ES_RETURN_SUCCESS;
}
#endif
return false;
}
bool EndpointSecurityAPI::UnmuteTargetPath(const Client &client, std::string_view path,
WatchItemPathType path_type) {
#if HAVE_MACOS_13
if (@available(macOS 13.0, *)) {
return es_unmute_path(client.Get(), path.data(),
path_type == WatchItemPathType::kPrefix
? ES_MUTE_PATH_TYPE_TARGET_PREFIX
: ES_MUTE_PATH_TYPE_TARGET_LITERAL) == ES_RETURN_SUCCESS;
}
#endif
return true;
}
bool EndpointSecurityAPI::RespondAuthResult(const Client &client, const Message &msg,
es_auth_result_t result, bool cache) {
return es_respond_auth_result(client.Get(), &(*msg), result, cache) == ES_RESPOND_RESULT_SUCCESS;
}
bool EndpointSecurityAPI::RespondFlagsResult(const Client &client, const Message &msg,
uint32_t allowed_flags, bool cache) {
return es_respond_flags_result(client.Get(), &(*msg), allowed_flags, cache);
}
bool EndpointSecurityAPI::MuteProcess(const Client &client, const audit_token_t *tok) {
return es_mute_process(client.Get(), tok) == ES_RETURN_SUCCESS;
}

View File

@@ -21,16 +21,30 @@
namespace santa::santad::event_providers::endpoint_security {
enum class EnrichOptions {
// Specifies default enricher operation.
kDefault,
// This option tells the enricher to only enrich with information that can be
// gathered without potentially triggering work from external processes.
kLocalOnly,
};
class Enricher {
public:
Enricher();
virtual ~Enricher() = default;
virtual std::shared_ptr<EnrichedMessage> Enrich(Message &&msg);
virtual EnrichedProcess Enrich(const es_process_t &es_proc);
virtual EnrichedFile Enrich(const es_file_t &es_file);
virtual EnrichedProcess Enrich(
const es_process_t &es_proc,
EnrichOptions options = EnrichOptions::kDefault);
virtual EnrichedFile Enrich(const es_file_t &es_file,
EnrichOptions options = EnrichOptions::kDefault);
virtual std::optional<std::shared_ptr<std::string>> UsernameForUID(uid_t uid);
virtual std::optional<std::shared_ptr<std::string>> UsernameForGID(gid_t gid);
virtual std::optional<std::shared_ptr<std::string>> UsernameForUID(
uid_t uid, EnrichOptions options = EnrichOptions::kDefault);
virtual std::optional<std::shared_ptr<std::string>> UsernameForGID(
gid_t gid, EnrichOptions options = EnrichOptions::kDefault);
private:
SantaCache<uid_t, std::optional<std::shared_ptr<std::string>>>

View File

@@ -81,26 +81,30 @@ std::shared_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
}
}
EnrichedProcess Enricher::Enrich(const es_process_t &es_proc) {
return EnrichedProcess(UsernameForUID(audit_token_to_euid(es_proc.audit_token)),
UsernameForGID(audit_token_to_egid(es_proc.audit_token)),
UsernameForUID(audit_token_to_ruid(es_proc.audit_token)),
UsernameForGID(audit_token_to_rgid(es_proc.audit_token)),
Enrich(*es_proc.executable));
EnrichedProcess Enricher::Enrich(const es_process_t &es_proc, EnrichOptions options) {
return EnrichedProcess(UsernameForUID(audit_token_to_euid(es_proc.audit_token), options),
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));
}
EnrichedFile Enricher::Enrich(const es_file_t &es_file) {
EnrichedFile Enricher::Enrich(const es_file_t &es_file, EnrichOptions options) {
// TODO(mlw): Consider having the enricher perform file hashing. This will
// make more sense if we start including hashes in more event types.
return EnrichedFile(UsernameForUID(es_file.stat.st_uid), UsernameForGID(es_file.stat.st_gid),
std::nullopt);
return EnrichedFile(UsernameForUID(es_file.stat.st_uid, options),
UsernameForGID(es_file.stat.st_gid, options), std::nullopt);
}
std::optional<std::shared_ptr<std::string>> Enricher::UsernameForUID(uid_t uid) {
std::optional<std::shared_ptr<std::string>> Enricher::UsernameForUID(uid_t uid,
EnrichOptions options) {
std::optional<std::shared_ptr<std::string>> username = username_cache_.get(uid);
if (username.has_value()) {
return username;
} else if (options == EnrichOptions::kLocalOnly) {
// If `kLocalOnly` option is set, do not attempt a lookup
return std::nullopt;
} else {
struct passwd *pw = getpwuid(uid);
if (pw) {
@@ -115,11 +119,15 @@ std::optional<std::shared_ptr<std::string>> Enricher::UsernameForUID(uid_t uid)
}
}
std::optional<std::shared_ptr<std::string>> Enricher::UsernameForGID(gid_t gid) {
std::optional<std::shared_ptr<std::string>> Enricher::UsernameForGID(gid_t gid,
EnrichOptions options) {
std::optional<std::shared_ptr<std::string>> groupname = groupname_cache_.get(gid);
if (groupname.has_value()) {
return groupname;
} else if (options == EnrichOptions::kLocalOnly) {
// If `kLocalOnly` option is set, do not attempt a lookup
return std::nullopt;
} else {
struct group *gr = getgrgid(gid);
if (gr) {

View File

@@ -34,8 +34,6 @@ class Message {
// Note: Safe to implement this, just not currently needed so left deleted.
Message& operator=(Message&& rhs) = delete;
// In macOS 10.15, es_retain_message/es_release_message were unsupported
// and required a full copy, which impacts performance if done too much...
Message(const Message& other);
Message& operator=(const Message& other) = delete;
@@ -47,10 +45,7 @@ class Message {
private:
std::shared_ptr<EndpointSecurityAPI> esapi_;
es_message_t* es_msg_;
mutable std::string pname_;
mutable std::string parent_pname_;
const es_message_t* es_msg_;
std::string GetProcessName(pid_t pid) const;
};

View File

@@ -22,8 +22,8 @@
namespace santa::santad::event_providers::endpoint_security {
Message::Message(std::shared_ptr<EndpointSecurityAPI> esapi, const es_message_t *es_msg)
: esapi_(esapi) {
es_msg_ = esapi_->RetainMessage(es_msg);
: esapi_(std::move(esapi)), es_msg_(es_msg) {
esapi_->RetainMessage(es_msg);
}
Message::~Message() {
@@ -45,10 +45,7 @@ Message::Message(const Message &other) {
}
std::string Message::ParentProcessName() const {
if (parent_pname_.length() == 0) {
parent_pname_ = GetProcessName(es_msg_->process->ppid);
}
return parent_pname_;
return GetProcessName(es_msg_->process->ppid);
}
std::string Message::GetProcessName(pid_t pid) const {

View File

@@ -65,7 +65,7 @@ pid_t AttemptToFindUnusedPID() {
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
mockESApi->SetExpectationsRetainReleaseMessage();
// Constructing a `Message` retains the underlying `es_message_t` and it is
// released when the `Message` object is destructed.
@@ -82,9 +82,7 @@ pid_t AttemptToFindUnusedPID() {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
EXPECT_CALL(*mockESApi, ReleaseMessage(testing::_))
.Times(2)
.After(EXPECT_CALL(*mockESApi, RetainMessage(testing::_))
.Times(2)
.WillRepeatedly(testing::Return(&esMsg)));
.After(EXPECT_CALL(*mockESApi, RetainMessage(testing::_)).Times(2));
{
Message msg1(mockESApi, &esMsg);
@@ -106,7 +104,7 @@ pid_t AttemptToFindUnusedPID() {
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
mockESApi->SetExpectationsRetainReleaseMessage();
// Search for an *existing* parent process.
{

View File

@@ -22,6 +22,7 @@
#include <set>
#include "Source/santad/DataLayer/WatchItemPolicy.h"
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
@@ -38,14 +39,32 @@ class MockEndpointSecurityAPI
MOCK_METHOD(bool, Subscribe,
(const santa::santad::event_providers::endpoint_security::Client &,
const std::set<es_event_type_t> &));
MOCK_METHOD(bool, UnsubscribeAll, (const Client &client));
MOCK_METHOD(es_message_t *, RetainMessage, (const es_message_t *msg));
MOCK_METHOD(void, ReleaseMessage, (es_message_t * msg));
MOCK_METHOD(bool, UnmuteAllPaths, (const Client &client));
MOCK_METHOD(bool, UnmuteAllTargetPaths, (const Client &client));
MOCK_METHOD(bool, IsTargetPathMutingInverted, (const Client &client));
MOCK_METHOD(bool, InvertTargetPathMuting, (const Client &client));
MOCK_METHOD(bool, MuteTargetPath,
(const Client &client, std::string_view path,
santa::santad::data_layer::WatchItemPathType path_type));
MOCK_METHOD(bool, UnmuteTargetPath,
(const Client &client, std::string_view path,
santa::santad::data_layer::WatchItemPathType path_type));
MOCK_METHOD(void, RetainMessage, (const es_message_t *msg));
MOCK_METHOD(void, ReleaseMessage, (const es_message_t *msg));
MOCK_METHOD(bool, RespondAuthResult,
(const santa::santad::event_providers::endpoint_security::Client &,
const santa::santad::event_providers::endpoint_security::Message &msg,
es_auth_result_t result, bool cache));
MOCK_METHOD(bool, RespondFlagsResult,
(const santa::santad::event_providers::endpoint_security::Client &client,
const santa::santad::event_providers::endpoint_security::Message &msg,
uint32_t allowed_flags, bool cache));
MOCK_METHOD(bool, MuteProcess,
(const santa::santad::event_providers::endpoint_security::Client &,
@@ -72,9 +91,9 @@ class MockEndpointSecurityAPI
EXPECT_CALL(*this, Subscribe).WillRepeatedly(testing::Return(true));
}
void SetExpectationsRetainReleaseMessage(es_message_t *msg) {
void SetExpectationsRetainReleaseMessage() {
EXPECT_CALL(*this, ReleaseMessage).Times(testing::AnyNumber());
EXPECT_CALL(*this, RetainMessage).WillRepeatedly(testing::Return(msg));
EXPECT_CALL(*this, RetainMessage).Times(testing::AnyNumber());
}
};

View File

@@ -0,0 +1,77 @@
/// Copyright 2022 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__EVENTPROVIDERS_RATELIMITER_H
#define SANTA__SANTAD__EVENTPROVIDERS_RATELIMITER_H
#import <Foundation/Foundation.h>
#include <atomic>
#include <memory>
#include "Source/santad/Metrics.h"
// Forward declarations
namespace santa::santad::event_providers {
class RateLimiterPeer;
}
namespace santa::santad::event_providers {
// Very basic rate limiting infrastructure.
// Currently only handles X events per duration.
//
// TODO(mlw): Support changing QPS via config
// TODO(mlw): Support per-rule QPS
// TODO(mlw): Consider adding sliding window support
class RateLimiter {
public:
// Factory
static std::shared_ptr<RateLimiter> Create(
std::shared_ptr<santa::santad::Metrics> metrics,
santa::santad::Processor processor, uint16_t max_qps,
NSTimeInterval reset_duration = kDefaultResetDuration);
RateLimiter(std::shared_ptr<santa::santad::Metrics> metrics,
santa::santad::Processor processor, uint16_t max_qps,
NSTimeInterval reset_duration);
enum class Decision {
kRateLimited = 0,
kAllowed,
};
Decision Decide(uint64_t cur_mach_time);
friend class santa::santad::event_providers::RateLimiterPeer;
private:
bool ShouldRateLimitLocked();
size_t EventsRateLimitedLocked();
void TryResetLocked(uint64_t cur_mach_time);
static constexpr NSTimeInterval kDefaultResetDuration = 15.0;
std::shared_ptr<santa::santad::Metrics> metrics_;
santa::santad::Processor processor_;
size_t log_count_total_ = 0;
size_t max_log_count_total_;
uint64_t reset_mach_time_;
uint64_t reset_duration_ns_;
dispatch_queue_t q_;
};
} // namespace santa::santad::event_providers
#endif

View File

@@ -0,0 +1,85 @@
/// Copyright 2022 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/RateLimiter.h"
#include "Source/common/BranchPrediction.h"
#include "Source/common/SystemResources.h"
using santa::santad::Metrics;
using santa::santad::Processor;
namespace santa::santad::event_providers {
std::shared_ptr<RateLimiter> RateLimiter::Create(std::shared_ptr<Metrics> metrics,
Processor processor, uint16_t max_qps,
NSTimeInterval reset_duration) {
return std::make_shared<RateLimiter>(std::move(metrics), processor, max_qps, reset_duration);
}
RateLimiter::RateLimiter(std::shared_ptr<Metrics> metrics, Processor processor, uint16_t max_qps,
NSTimeInterval reset_duration)
: metrics_(std::move(metrics)),
processor_(processor),
max_log_count_total_(reset_duration * max_qps),
reset_mach_time_(0),
reset_duration_ns_(reset_duration * NSEC_PER_SEC) {
q_ = dispatch_queue_create(
"com.google.santa.daemon.rate_limiter",
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
QOS_CLASS_USER_INTERACTIVE, 0));
}
bool RateLimiter::ShouldRateLimitLocked() {
return log_count_total_ > max_log_count_total_;
}
size_t RateLimiter::EventsRateLimitedLocked() {
if (unlikely(ShouldRateLimitLocked())) {
return log_count_total_ - max_log_count_total_;
} else {
return 0;
}
}
void RateLimiter::TryResetLocked(uint64_t cur_mach_time) {
if (cur_mach_time > reset_mach_time_) {
if (metrics_) {
metrics_->SetRateLimitingMetrics(processor_, EventsRateLimitedLocked());
}
log_count_total_ = 0;
reset_mach_time_ = AddNanosecondsToMachTime(reset_duration_ns_, cur_mach_time);
}
}
RateLimiter::Decision RateLimiter::Decide(uint64_t cur_mach_time) {
__block RateLimiter::Decision decision;
dispatch_sync(q_, ^{
TryResetLocked(cur_mach_time);
++log_count_total_;
if (unlikely(ShouldRateLimitLocked())) {
decision = Decision::kRateLimited;
} else {
decision = RateLimiter::Decision::kAllowed;
}
});
return decision;
}
} // namespace santa::santad::event_providers

View File

@@ -0,0 +1,149 @@
/// Copyright 2022 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/RateLimiter.h"
#include <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#include "Source/common/SystemResources.h"
#include "Source/santad/Metrics.h"
using santa::santad::event_providers::RateLimiter;
static const santa::santad::Processor kDefaultProcessor =
santa::santad::Processor::kFileAccessAuthorizer;
namespace santa::santad::event_providers {
class RateLimiterPeer : public RateLimiter {
public:
using RateLimiter::RateLimiter;
using RateLimiter::EventsRateLimitedLocked;
using RateLimiter::ShouldRateLimitLocked;
using RateLimiter::TryResetLocked;
using RateLimiter::log_count_total_;
using RateLimiter::reset_mach_time_;
};
} // namespace santa::santad::event_providers
using santa::santad::event_providers::RateLimiterPeer;
@interface RateLimiterTest : XCTestCase
@end
@implementation RateLimiterTest
- (void)testTryResetLocked {
// Create an object supporting 1 QPS, and a reset duration of 2s
uint16_t maxQps = 1;
NSTimeInterval resetDuration = 2;
RateLimiterPeer rlp(nullptr, kDefaultProcessor, maxQps, resetDuration);
// Check the current reset_mach_time_ is 0 so that it gets
// set when the first decision is made
XCTAssertEqual(rlp.reset_mach_time_, 0);
// Define our current mach time and create the expected new reset duration floor
uint64_t curMachTime = 1;
uint64_t expectedMachTime = AddNanosecondsToMachTime(resetDuration, curMachTime);
// Set a higher log count to ensure it is reset
rlp.log_count_total_ = 123;
rlp.TryResetLocked(curMachTime);
// Ensure values are reset appropriately
XCTAssertEqual(rlp.log_count_total_, 0);
XCTAssertGreaterThanOrEqual(rlp.reset_mach_time_, expectedMachTime);
// Setup values so that calling TryResetLocked shouldn't reset anything
size_t expectedLogCount = 123;
expectedMachTime = 456;
rlp.log_count_total_ = expectedLogCount;
rlp.reset_mach_time_ = expectedMachTime;
curMachTime = rlp.reset_mach_time_;
rlp.TryResetLocked(curMachTime);
// Ensure the values were not changed
XCTAssertEqual(rlp.log_count_total_, expectedLogCount);
XCTAssertGreaterThanOrEqual(rlp.reset_mach_time_, expectedMachTime);
}
- (void)testDecide {
// Create an object supporting 2 QPS, and a reset duration of 4s
uint16_t maxQps = 2;
NSTimeInterval resetDuration = 4;
uint64_t allowedLogsPerDuration = maxQps * resetDuration;
RateLimiterPeer rlp(nullptr, kDefaultProcessor, maxQps, resetDuration);
// Check the current log count is initially 0
XCTAssertEqual(rlp.log_count_total_, 0);
// Make the first decision
RateLimiter::Decision gotDecision;
for (uint64_t i = 0; i < (allowedLogsPerDuration); i++) {
gotDecision = rlp.Decide(0);
XCTAssertEqual(gotDecision, RateLimiter::Decision::kAllowed);
}
// Ensure the log count is the expected amount
XCTAssertEqual(rlp.log_count_total_, allowedLogsPerDuration);
// Make another decision and ensure the log count still increases and
// the decision is rate limited
gotDecision = rlp.Decide(0);
XCTAssertEqual(gotDecision, RateLimiter::Decision::kRateLimited);
XCTAssertEqual(rlp.log_count_total_, allowedLogsPerDuration + 1);
// Make another decision, though now with the cur mach time greater than
// the reset mach time. Then ensure values were appropriately reset.
uint64_t oldResetMachTime = rlp.reset_mach_time_;
gotDecision = rlp.Decide(rlp.reset_mach_time_ + 1);
XCTAssertEqual(gotDecision, RateLimiter::Decision::kAllowed);
XCTAssertEqual(rlp.log_count_total_, 1);
XCTAssertGreaterThan(rlp.reset_mach_time_, oldResetMachTime);
}
- (void)testShouldRateLimitAndCounts {
// Create an object supporting 2 QPS, and a reset duration of 4s
uint16_t maxQps = 2;
NSTimeInterval resetDuration = 4;
uint64_t allowedLogsPerDuration = maxQps * resetDuration;
uint64_t logsOverQPS = 5;
RateLimiterPeer rlp(nullptr, kDefaultProcessor, maxQps, resetDuration);
// Initially no rate limiting should apply
XCTAssertFalse(rlp.ShouldRateLimitLocked());
XCTAssertEqual(rlp.EventsRateLimitedLocked(), 0);
// Simulate a smmaller volume of logs received than QPS
rlp.log_count_total_ = allowedLogsPerDuration - 1;
XCTAssertFalse(rlp.ShouldRateLimitLocked());
XCTAssertEqual(rlp.EventsRateLimitedLocked(), 0);
// Simulate a larger volume of logs received than QPS
rlp.log_count_total_ = allowedLogsPerDuration + logsOverQPS;
XCTAssertTrue(rlp.ShouldRateLimitLocked());
XCTAssertEqual(rlp.EventsRateLimitedLocked(), logsOverQPS);
}
@end

View File

@@ -18,6 +18,8 @@
#include <os/base.h>
#include <stdlib.h>
#import "Source/common/BranchPrediction.h"
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTLogging.h"
#include "Source/santad/EventProviders/AuthResultCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
@@ -64,15 +66,15 @@ using santa::santad::event_providers::endpoint_security::Message;
const es_file_t *targetFile = msg->event.exec.target->executable;
while (true) {
santa_action_t returnAction = self->_authResultCache->CheckCache(targetFile);
SNTAction returnAction = self->_authResultCache->CheckCache(targetFile);
if (RESPONSE_VALID(returnAction)) {
es_auth_result_t authResult = ES_AUTH_RESULT_DENY;
switch (returnAction) {
case ACTION_RESPOND_ALLOW_COMPILER:
case SNTActionRespondAllowCompiler:
[self.compilerController setProcess:msg->event.exec.target->audit_token isCompiler:true];
OS_FALLTHROUGH;
case ACTION_RESPOND_ALLOW: authResult = ES_AUTH_RESULT_ALLOW; break;
case SNTActionRespondAllow: authResult = ES_AUTH_RESULT_ALLOW; break;
default: break;
}
@@ -80,7 +82,7 @@ using santa::santad::event_providers::endpoint_security::Message;
withAuthResult:authResult
cacheable:(authResult == ES_AUTH_RESULT_ALLOW)];
return;
} else if (returnAction == ACTION_REQUEST_BINARY) {
} else if (returnAction == SNTActionRequestBinary) {
// TODO(mlw): Add a metric here to observe how ofthen this happens in practice.
// TODO(mlw): Look into caching a `Deferred<value>` to better prevent
// raciness of multiple threads checking the cache simultaneously.
@@ -91,10 +93,10 @@ using santa::santad::event_providers::endpoint_security::Message;
}
}
self->_authResultCache->AddToCache(targetFile, ACTION_REQUEST_BINARY);
self->_authResultCache->AddToCache(targetFile, SNTActionRequestBinary);
[self.execController validateExecEvent:msg
postAction:^bool(santa_action_t action) {
postAction:^bool(SNTAction action) {
return [self postAction:action forMessage:msg];
}];
}
@@ -109,7 +111,7 @@ using santa::santad::event_providers::endpoint_security::Message;
}
if (![self.execController synchronousShouldProcessExecEvent:esMsg]) {
[self postAction:ACTION_RESPOND_DENY forMessage:esMsg];
[self postAction:SNTActionRespondDeny forMessage:esMsg];
recordEventMetrics(EventDisposition::kDropped);
return;
}
@@ -121,19 +123,19 @@ using santa::santad::event_providers::endpoint_security::Message;
}];
}
- (bool)postAction:(santa_action_t)action forMessage:(const Message &)esMsg {
- (bool)postAction:(SNTAction)action forMessage:(const Message &)esMsg {
es_auth_result_t authResult;
switch (action) {
case ACTION_RESPOND_ALLOW_COMPILER:
case SNTActionRespondAllowCompiler:
[self.compilerController setProcess:esMsg->event.exec.target->audit_token isCompiler:true];
OS_FALLTHROUGH;
case ACTION_RESPOND_ALLOW: authResult = ES_AUTH_RESULT_ALLOW; break;
case ACTION_RESPOND_DENY: authResult = ES_AUTH_RESULT_DENY; break;
case SNTActionRespondAllow: authResult = ES_AUTH_RESULT_ALLOW; break;
case SNTActionRespondDeny: authResult = ES_AUTH_RESULT_DENY; break;
default:
// This is a programming error. Bail.
LOGE(@"Invalid action for postAction, exiting.");
[NSException raise:@"Invalid post action" format:@"Invalid post action: %d", action];
[NSException raise:@"Invalid post action" format:@"Invalid post action: %ld", action];
}
self->_authResultCache->AddToCache(esMsg->event.exec.target->executable, action);

View File

@@ -22,6 +22,7 @@
#include <memory>
#include <set>
#include "Source/common/SNTCommonEnums.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/EventProviders/AuthResultCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
@@ -40,13 +41,13 @@ class MockAuthResultCache : public AuthResultCache {
public:
using AuthResultCache::AuthResultCache;
MOCK_METHOD(bool, AddToCache, (const es_file_t *es_file, santa_action_t decision));
MOCK_METHOD(santa_action_t, CheckCache, (const es_file_t *es_file));
MOCK_METHOD(bool, AddToCache, (const es_file_t *es_file, SNTAction decision));
MOCK_METHOD(SNTAction, CheckCache, (const es_file_t *es_file));
};
@interface SNTEndpointSecurityAuthorizer (Testing)
- (void)processMessage:(const Message &)msg;
- (bool)postAction:(santa_action_t)action forMessage:(const Message &)esMsg;
- (bool)postAction:(SNTAction)action forMessage:(const Message &)esMsg;
@end
@interface SNTEndpointSecurityAuthorizerTest : XCTestCase
@@ -84,34 +85,41 @@ class MockAuthResultCache : public AuthResultCache {
}
- (void)testHandleMessage {
#ifdef THREAD_SANITIZER
// TSAN and this test do not get along in multiple ways.
// We get data race false positives in OCMock, and timeouts
// waiting for messages processing (presumably due to tsan's scheduling).
// Just skip it.
XCTSkip(@"TSAN enabled");
return;
#endif
es_file_t file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&file);
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsESNewClient();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
// There is a benign leak of the mock object in this test.
// `handleMessage:recordEventMetrics:` will call `processMessage:handler:` in the parent
// class. This will dispatch to two blocks and create message copies. The block that
// handles `deadline` timeouts will not complete before the test finishes, and the
// mock object will think that it has been leaked.
::testing::Mock::AllowLeak(mockESApi.get());
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
SNTEndpointSecurityAuthorizer *authClient =
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
metrics:nullptr
execController:self.mockExecController
compilerController:nil
authResultCache:nullptr];
id mockAuthClient = OCMPartialMock(authClient);
// Test unhandled event type
{
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsESNewClient();
mockESApi->SetExpectationsRetainReleaseMessage();
// There is a benign leak of the mock object in this test.
// `handleMessage:recordEventMetrics:` will call `processMessage:handler:` in the parent
// class. This will dispatch to two blocks and create message copies. The block that
// handles `deadline` timeouts will not complete before the test finishes, and the
// mock object will think that it has been leaked.
::testing::Mock::AllowLeak(mockESApi.get());
SNTEndpointSecurityAuthorizer *authClient =
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
metrics:nullptr
execController:self.mockExecController
compilerController:nil
authResultCache:nullptr];
// Temporarily change the event type
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC;
XCTAssertThrows([authClient handleMessage:Message(mockESApi, &esMsg)
@@ -119,59 +127,97 @@ class MockAuthResultCache : public AuthResultCache {
XCTFail("Unhandled event types shouldn't call metrics recorder");
}]);
esMsg.event_type = ES_EVENT_TYPE_AUTH_EXEC;
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
// Test SNTExecutionController determines the event shouldn't be processed
{
Message msg(mockESApi, &esMsg);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsESNewClient();
mockESApi->SetExpectationsRetainReleaseMessage();
::testing::Mock::AllowLeak(mockESApi.get());
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
.ignoringNonObjectArgs()
.andReturn(NO);
SNTEndpointSecurityAuthorizer *authClient =
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
metrics:nullptr
execController:self.mockExecController
compilerController:nil
authResultCache:nullptr];
OCMExpect([mockAuthClient postAction:ACTION_RESPOND_DENY forMessage:Message(mockESApi, &esMsg)])
.ignoringNonObjectArgs();
OCMStub([mockAuthClient postAction:ACTION_RESPOND_DENY forMessage:Message(mockESApi, &esMsg)])
.ignoringNonObjectArgs()
.andDo(nil);
id mockAuthClient = OCMPartialMock(authClient);
[mockAuthClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kDropped);
dispatch_semaphore_signal(semaMetrics);
}];
// Scope so msg is destructed (and calls ReleaseMessage) before stopMocking is called.
{
Message msg(mockESApi, &esMsg);
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
.ignoringNonObjectArgs()
.andReturn(NO);
OCMExpect([mockAuthClient postAction:SNTActionRespondDeny
forMessage:Message(mockESApi, &esMsg)])
.ignoringNonObjectArgs();
OCMStub([mockAuthClient postAction:SNTActionRespondDeny
forMessage:Message(mockESApi, &esMsg)])
.ignoringNonObjectArgs()
.andDo(nil);
[mockAuthClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kDropped);
dispatch_semaphore_signal(semaMetrics);
}];
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
}
[mockAuthClient stopMocking];
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
// Test SNTExecutionController determines the event should be processed and
// processMessage:handler: is called.
{
Message msg(mockESApi, &esMsg);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsESNewClient();
mockESApi->SetExpectationsRetainReleaseMessage();
::testing::Mock::AllowLeak(mockESApi.get());
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
.ignoringNonObjectArgs()
.andReturn(YES);
SNTEndpointSecurityAuthorizer *authClient =
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
metrics:nullptr
execController:self.mockExecController
compilerController:nil
authResultCache:nullptr];
OCMExpect([mockAuthClient processMessage:Message(mockESApi, &esMsg)]).ignoringNonObjectArgs();
OCMStub([mockAuthClient processMessage:Message(mockESApi, &esMsg)])
.ignoringNonObjectArgs()
.andDo(nil);
id mockAuthClient = OCMPartialMock(authClient);
[mockAuthClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kProcessed);
dispatch_semaphore_signal(semaMetrics);
}];
{
Message msg(mockESApi, &esMsg);
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
.ignoringNonObjectArgs()
.andReturn(YES);
OCMExpect([mockAuthClient processMessage:Message(mockESApi, &esMsg)]).ignoringNonObjectArgs();
OCMStub([mockAuthClient processMessage:Message(mockESApi, &esMsg)])
.ignoringNonObjectArgs()
.andDo(nil);
[mockAuthClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kProcessed);
dispatch_semaphore_signal(semaMetrics);
}];
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
}
[mockAuthClient stopMocking];
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
[mockAuthClient stopMocking];
}
- (void)testProcessMessageWaitThenAllow {
@@ -186,15 +232,15 @@ class MockAuthResultCache : public AuthResultCache {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsESNewClient();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
mockESApi->SetExpectationsRetainReleaseMessage();
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
EXPECT_CALL(*mockAuthCache, CheckCache)
.WillOnce(testing::Return(ACTION_REQUEST_BINARY))
.WillOnce(testing::Return(ACTION_REQUEST_BINARY))
.WillOnce(testing::Return(ACTION_RESPOND_ALLOW_COMPILER))
.WillOnce(testing::Return(ACTION_UNSET));
EXPECT_CALL(*mockAuthCache, AddToCache(testing::_, ACTION_REQUEST_BINARY))
.WillOnce(testing::Return(SNTActionRequestBinary))
.WillOnce(testing::Return(SNTActionRequestBinary))
.WillOnce(testing::Return(SNTActionRespondAllowCompiler))
.WillOnce(testing::Return(SNTActionUnset));
EXPECT_CALL(*mockAuthCache, AddToCache(testing::_, SNTActionRequestBinary))
.WillOnce(testing::Return(true));
id mockCompilerController = OCMStrictClassMock([SNTCompilerController class]);
@@ -210,7 +256,7 @@ class MockAuthResultCache : public AuthResultCache {
// This block tests that processing is held up until an outstanding thread
// processing another event completes and returns a result. This test
// specifically will check the `ACTION_RESPOND_ALLOW_COMPILER` flow.
// specifically will check the `SNTActionRespondAllowCompiler` flow.
{
Message msg(mockESApi, &esMsg);
OCMExpect([mockAuthClient respondToMessage:msg
@@ -253,14 +299,14 @@ class MockAuthResultCache : public AuthResultCache {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsESNewClient();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
mockESApi->SetExpectationsRetainReleaseMessage();
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, ACTION_RESPOND_ALLOW_COMPILER))
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, SNTActionRespondAllowCompiler))
.WillOnce(testing::Return(true));
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, ACTION_RESPOND_ALLOW))
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, SNTActionRespondAllow))
.WillOnce(testing::Return(true));
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, ACTION_RESPOND_DENY))
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, SNTActionRespondDeny))
.WillOnce(testing::Return(true));
id mockCompilerController = OCMStrictClassMock([SNTCompilerController class]);
@@ -277,12 +323,12 @@ class MockAuthResultCache : public AuthResultCache {
{
Message msg(mockESApi, &esMsg);
XCTAssertThrows([mockAuthClient postAction:(santa_action_t)123 forMessage:msg]);
XCTAssertThrows([mockAuthClient postAction:(SNTAction)123 forMessage:msg]);
std::map<santa_action_t, es_auth_result_t> actions = {
{ACTION_RESPOND_ALLOW_COMPILER, ES_AUTH_RESULT_ALLOW},
{ACTION_RESPOND_ALLOW, ES_AUTH_RESULT_ALLOW},
{ACTION_RESPOND_DENY, ES_AUTH_RESULT_DENY},
std::map<SNTAction, es_auth_result_t> actions = {
{SNTActionRespondAllowCompiler, ES_AUTH_RESULT_ALLOW},
{SNTActionRespondAllow, ES_AUTH_RESULT_ALLOW},
{SNTActionRespondDeny, ES_AUTH_RESULT_DENY},
};
for (const auto &kv : actions) {

View File

@@ -21,9 +21,15 @@
#include <stdlib.h>
#include <sys/qos.h>
#import "Source/common/SNTCommon.h"
#include <set>
#include <string>
#include <string_view>
#include "Source/common/BranchPrediction.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTLogging.h"
#include "Source/common/SystemResources.h"
#include "Source/santad/DataLayer/WatchItemPolicy.h"
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
@@ -32,20 +38,24 @@
using santa::santad::EventDisposition;
using santa::santad::Metrics;
using santa::santad::Processor;
using santa::santad::data_layer::WatchItemPathType;
using santa::santad::event_providers::endpoint_security::Client;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
using santa::santad::event_providers::endpoint_security::Message;
constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db",
"/private/var/db/santa/events.db"};
@interface SNTEndpointSecurityClient ()
@property int64_t deadlineMarginMS;
@property SNTConfigurator *configurator;
@end
@implementation SNTEndpointSecurityClient {
std::shared_ptr<EndpointSecurityAPI> _esApi;
std::shared_ptr<Metrics> _metrics;
Client _esClient;
mach_timebase_info_data_t _timebase;
dispatch_queue_t _authQueue;
dispatch_queue_t _notifyQueue;
Processor _processor;
@@ -59,14 +69,9 @@ using santa::santad::event_providers::endpoint_security::Message;
_esApi = std::move(esApi);
_metrics = std::move(metrics);
_deadlineMarginMS = 5000;
_configurator = [SNTConfigurator configurator];
_processor = processor;
if (mach_timebase_info(&_timebase) != KERN_SUCCESS) {
LOGE(@"Failed to get mach timebase info");
// Assumed to be transitory failure. Let the daemon restart.
exit(EXIT_FAILURE);
}
_authQueue = dispatch_queue_create(
"com.google.santa.daemon.auth_queue",
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL,
@@ -100,9 +105,8 @@ using santa::santad::event_providers::endpoint_security::Message;
[self doesNotRecognizeSelector:_cmd];
}
- (BOOL)shouldHandleMessage:(const Message &)esMsg
ignoringOtherESClients:(BOOL)ignoringOtherESClients {
if (esMsg->process->is_es_client && ignoringOtherESClients) {
- (BOOL)shouldHandleMessage:(const Message &)esMsg {
if (esMsg->process->is_es_client && [self.configurator ignoreOtherEndpointSecurityClients]) {
if (esMsg->action_type == ES_ACTION_TYPE_AUTH) {
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:true];
}
@@ -122,9 +126,7 @@ using santa::santad::event_providers::endpoint_security::Message;
self->_esClient = self->_esApi->NewClient(^(es_client_t *c, Message esMsg) {
int64_t processingStart = clock_gettime_nsec_np(CLOCK_MONOTONIC);
es_event_type_t eventType = esMsg->event_type;
if ([self shouldHandleMessage:esMsg
ignoringOtherESClients:[[SNTConfigurator configurator]
ignoreOtherEndpointSecurityClients]]) {
if ([self shouldHandleMessage:esMsg]) {
[self handleMessage:std::move(esMsg)
recordEventMetrics:^(EventDisposition disposition) {
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
@@ -187,16 +189,65 @@ using santa::santad::event_providers::endpoint_security::Message;
return [self subscribe:events] && [self clearCache];
}
- (bool)unsubscribeAll {
return _esApi->UnsubscribeAll(_esClient);
}
- (bool)unmuteEverything {
bool result = _esApi->UnmuteAllPaths(_esClient);
result = _esApi->UnmuteAllTargetPaths(_esClient) && result;
return result;
}
- (bool)enableTargetPathWatching {
return _esApi->InvertTargetPathMuting(_esClient);
}
- (bool)muteTargetPaths:(const std::vector<std::pair<std::string, WatchItemPathType>> &)paths {
bool result = true;
for (const auto &pathAndTypePair : paths) {
result =
_esApi->MuteTargetPath(_esClient, pathAndTypePair.first, pathAndTypePair.second) && result;
}
return result;
}
- (bool)unmuteTargetPaths:(const std::vector<std::pair<std::string, WatchItemPathType>> &)paths {
bool result = true;
for (const auto &pathAndTypePair : paths) {
result =
_esApi->UnmuteTargetPath(_esClient, pathAndTypePair.first, pathAndTypePair.second) && result;
}
return result;
}
- (bool)respondToMessage:(const Message &)msg
withAuthResult:(es_auth_result_t)result
cacheable:(bool)cacheable {
return _esApi->RespondAuthResult(_esClient, msg, result, cacheable);
if (msg->event_type == ES_EVENT_TYPE_AUTH_OPEN) {
return _esApi->RespondFlagsResult(
// For now, Santa is only concerned about alllowing all access or no
// access, hence the flags being translated here to all or nothing based
// on the auth result. In the future it might be beneficial to expand the
// scope of Santa to enforce things like read-only access.
_esClient, msg, (result == ES_AUTH_RESULT_ALLOW) ? 0xffffffff : 0x0, cacheable);
} else {
return _esApi->RespondAuthResult(_esClient, msg, result, cacheable);
}
}
- (void)processEnrichedMessage:(std::shared_ptr<EnrichedMessage>)msg
handler:(void (^)(std::shared_ptr<EnrichedMessage>))messageHandler {
__block std::shared_ptr<EnrichedMessage> msgTmp = std::move(msg);
dispatch_async(_notifyQueue, ^{
messageHandler(std::move(msg));
messageHandler(std::move(msgTmp));
});
}
- (void)asynchronouslyProcess:(Message)msg handler:(void (^)(Message &&))messageHandler {
__block Message msgTmp = std::move(msg);
dispatch_async(_notifyQueue, ^{
messageHandler(std::move(msgTmp));
});
}
@@ -216,15 +267,14 @@ using santa::santad::event_providers::endpoint_security::Message;
dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0);
const uint64_t timeout = NSEC_PER_MSEC * (self.deadlineMarginMS);
uint64_t deadlineMachTime = msg->deadline - mach_absolute_time();
uint64_t deadlineNano = deadlineMachTime * _timebase.numer / _timebase.denom;
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).
// Workaround for compiler bug that doesn't properly close over variables
// Note: On macOS 10.15 this will cause extra message copies.
__block Message processMsg = msg;
__block Message deadlineMsg = msg;
@@ -245,7 +295,7 @@ using santa::santad::event_providers::endpoint_security::Message;
});
dispatch_async(self->_authQueue, ^{
messageHandler(deadlineMsg);
messageHandler(processMsg);
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
// Deadline expired, wait for deadline block to finish.
dispatch_semaphore_wait(deadlineExpiredSema, DISPATCH_TIME_FOREVER);
@@ -253,12 +303,28 @@ using santa::santad::event_providers::endpoint_security::Message;
});
}
+ (bool)isDatabasePath:(const std::string_view)path {
+ (std::set<std::string>)getProtectedPaths {
std::set<std::string> protectedPathsCopy;
for (size_t i = 0; i < sizeof(kProtectedFiles) / sizeof(kProtectedFiles[0]); i++) {
protectedPathsCopy.insert(std::string(kProtectedFiles[i]));
}
return protectedPathsCopy;
}
+ (bool)isProtectedPath:(const std::string_view)path {
// TODO(mlw): These values should come from `SNTDatabaseController`. But right
// now they live as NSStrings. We should make them `std::string_view` types
// in order to use them here efficiently, but will need to make the
// `SNTDatabaseController` an ObjC++ file.
return (path == "/private/var/db/santa/rules.db" || path == "/private/var/db/santa/events.db");
for (size_t i = 0; i < sizeof(kProtectedFiles) / sizeof(kProtectedFiles[0]); i++) {
if (path == kProtectedFiles[i]) {
return true;
}
}
return false;
}
@end

View File

@@ -17,9 +17,11 @@
#include <memory>
#include <string>
#include <vector>
#import <Foundation/Foundation.h>
#include "Source/santad/DataLayer/WatchItemPolicy.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
@@ -46,12 +48,24 @@
/// subscribing mitigates this posibility.
- (bool)subscribeAndClearCache:(const std::set<es_event_type_t> &)events;
- (bool)unsubscribeAll;
- (bool)unmuteEverything;
- (bool)enableTargetPathWatching;
- (bool)muteTargetPaths:
(const std::vector<std::pair<std::string, santa::santad::data_layer::WatchItemPathType>> &)paths;
- (bool)unmuteTargetPaths:
(const std::vector<std::pair<std::string, santa::santad::data_layer::WatchItemPathType>> &)paths;
/// Responds to the Message with the given auth result
///
/// @param Message The wrapped es_message_t being responded to
/// @param result Either ES_AUTH_RESULT_ALLOW or ES_AUTH_RESULT_DENY
/// @param cacheable true if ES should attempt to cache the result, otherwise false
/// @return true if the response was successful, otherwise false
///
/// @note If the msg event type requires a flags response, the correct ES API will automatically
/// be called. ALLOWED results will be translated to having all flags set, and DENIED results
/// will be translated to having all flags cleared.
- (bool)respondToMessage:(const santa::santad::event_providers::endpoint_security::Message &)msg
withAuthResult:(es_auth_result_t)result
cacheable:(bool)cacheable;
@@ -64,6 +78,11 @@
santa::santad::event_providers::endpoint_security::EnrichedMessage>))
messageHandler;
- (void)asynchronouslyProcess:(santa::santad::event_providers::endpoint_security::Message)msg
handler:
(void (^)(santa::santad::event_providers::endpoint_security::Message &&))
messageHandler;
- (void)processMessage:(santa::santad::event_providers::endpoint_security::Message &&)msg
handler:
(void (^)(const santa::santad::event_providers::endpoint_security::Message &))
@@ -71,7 +90,8 @@
- (bool)clearCache;
+ (bool)isDatabasePath:(const std::string_view)path;
+ (std::set<std::string>)getProtectedPaths;
+ (bool)isProtectedPath:(const std::string_view)path;
+ (bool)populateAuditTokenSelf:(audit_token_t *)tok;
@end

View File

@@ -22,7 +22,9 @@
#include <memory>
#import "Source/common/SNTConfigurator.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/DataLayer/WatchItemPolicy.h"
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
@@ -31,6 +33,7 @@
#include "Source/santad/Metrics.h"
using santa::santad::Processor;
using santa::santad::data_layer::WatchItemPathType;
using santa::santad::event_providers::endpoint_security::Client;
using santa::santad::event_providers::endpoint_security::EnrichedClose;
using santa::santad::event_providers::endpoint_security::EnrichedFile;
@@ -44,8 +47,7 @@ using santa::santad::event_providers::endpoint_security::Message;
- (NSString *)errorMessageForNewClientResult:(es_new_client_result_t)result;
- (void)handleMessage:(Message &&)esMsg
recordEventMetrics:(void (^)(santa::santad::EventDisposition disposition))recordEventMetrics;
- (BOOL)shouldHandleMessage:(const Message &)esMsg
ignoringOtherESClients:(BOOL)ignoringOtherESClients;
- (BOOL)shouldHandleMessage:(const Message &)esMsg;
@property int64_t deadlineMarginMS;
@end
@@ -104,7 +106,7 @@ using santa::santad::event_providers::endpoint_security::Message;
es_message_t esMsg;
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
mockESApi->SetExpectationsRetainReleaseMessage();
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
@@ -122,12 +124,15 @@ using santa::santad::event_providers::endpoint_security::Message;
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_FORK, &proc);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
mockESApi->SetExpectationsRetainReleaseMessage();
// Have subscribe fail the first time, meaning clear cache only called once.
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, ES_AUTH_RESULT_ALLOW, true))
.WillOnce(testing::Return(true));
id mockConfigurator = OCMStrictClassMock([SNTConfigurator class]);
OCMStub([mockConfigurator configurator]).andReturn(mockConfigurator);
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
@@ -137,24 +142,31 @@ using santa::santad::event_providers::endpoint_security::Message;
Message msg(mockESApi, &esMsg);
// Is ES client, but don't ignore others == Should Handle
OCMExpect([mockConfigurator ignoreOtherEndpointSecurityClients]).andReturn(NO);
esMsg.process->is_es_client = true;
XCTAssertTrue([client shouldHandleMessage:msg ignoringOtherESClients:NO]);
XCTAssertTrue([client shouldHandleMessage:msg]);
// Not ES client, but ignore others == Should Handle
// Don't setup configurator mock since it won't be called when `is_es_client` is false
esMsg.process->is_es_client = false;
XCTAssertTrue([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
XCTAssertTrue([client shouldHandleMessage:msg]);
// Is ES client, don't ignore others, and non-AUTH == Don't Handle
OCMExpect([mockConfigurator ignoreOtherEndpointSecurityClients]).andReturn(YES);
esMsg.process->is_es_client = true;
XCTAssertFalse([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
XCTAssertFalse([client shouldHandleMessage:msg]);
// Is ES client, don't ignore others, and AUTH == Respond and Don't Handle
OCMExpect([mockConfigurator ignoreOtherEndpointSecurityClients]).andReturn(YES);
esMsg.process->is_es_client = true;
esMsg.action_type = ES_ACTION_TYPE_AUTH;
XCTAssertFalse([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
XCTAssertFalse([client shouldHandleMessage:msg]);
}
XCTAssertTrue(OCMVerifyAll(mockConfigurator));
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
[mockConfigurator stopMocking];
}
- (void)testPopulateAuditTokenSelf {
@@ -244,10 +256,123 @@ using santa::santad::event_providers::endpoint_security::Message;
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testUnsubscribeAll {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
// Test the underlying unsubscribe all impl returning both true and false
EXPECT_CALL(*mockESApi, UnsubscribeAll)
.WillOnce(testing::Return(true))
.WillOnce(testing::Return(false));
XCTAssertTrue([client unsubscribeAll]);
XCTAssertFalse([client unsubscribeAll]);
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testUnmuteEverything {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
// Test variations of underlying unmute impls returning both true and false
EXPECT_CALL(*mockESApi, UnmuteAllPaths)
.WillOnce(testing::Return(true))
.WillOnce(testing::Return(false));
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths)
.WillOnce(testing::Return(true))
.WillOnce(testing::Return(true));
XCTAssertTrue([client unmuteEverything]);
XCTAssertFalse([client unmuteEverything]);
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testEnableTargetPathWatching {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
// Test the underlying invert nute impl returning both true and false
EXPECT_CALL(*mockESApi, InvertTargetPathMuting)
.WillOnce(testing::Return(true))
.WillOnce(testing::Return(false));
XCTAssertTrue([client enableTargetPathWatching]);
XCTAssertFalse([client enableTargetPathWatching]);
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testMuteTargetPaths {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
// 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))
.WillOnce(testing::Return(true));
EXPECT_CALL(*mockESApi, MuteTargetPath(testing::_, "b", WatchItemPathType::kLiteral))
.WillOnce(testing::Return(false));
EXPECT_CALL(*mockESApi, MuteTargetPath(testing::_, "c", WatchItemPathType::kPrefix))
.WillOnce(testing::Return(true));
std::vector<std::pair<std::string, WatchItemPathType>> paths = {
{"a", WatchItemPathType::kLiteral},
{"b", WatchItemPathType::kLiteral},
{"c", WatchItemPathType::kPrefix},
};
XCTAssertFalse([client muteTargetPaths:paths]);
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testUnmuteTargetPaths {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
// 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))
.WillOnce(testing::Return(true));
EXPECT_CALL(*mockESApi, UnmuteTargetPath(testing::_, "b", WatchItemPathType::kLiteral))
.WillOnce(testing::Return(false));
EXPECT_CALL(*mockESApi, UnmuteTargetPath(testing::_, "c", WatchItemPathType::kPrefix))
.WillOnce(testing::Return(true));
std::vector<std::pair<std::string, WatchItemPathType>> paths = {
{"a", WatchItemPathType::kLiteral},
{"b", WatchItemPathType::kLiteral},
{"c", WatchItemPathType::kPrefix},
};
XCTAssertFalse([client unmuteTargetPaths:paths]);
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testRespondToMessageWithAuthResultCacheable {
es_message_t esMsg;
esMsg.event_type = ES_EVENT_TYPE_AUTH_EXEC;
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
mockESApi->SetExpectationsRetainReleaseMessage();
es_auth_result_t result = ES_AUTH_RESULT_DENY;
bool cacheable = true;
@@ -270,44 +395,41 @@ using santa::santad::event_providers::endpoint_security::Message;
}
- (void)testProcessEnrichedMessageHandler {
es_message_t esMsg;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
// Note: In this test, `RetainMessage` isn't setup to return anything. This
// means that the underlying `es_msg_` in the `Message` object is NULL, and
// therefore no call to `ReleaseMessage` is ever made (hence no expectations).
// Because we don't need to operate on the es_msg_, this simplifies the test.
EXPECT_CALL(*mockESApi, RetainMessage);
mockESApi->SetExpectationsRetainReleaseMessage();
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
{
auto enrichedMsg = std::make_shared<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)));
es_message_t esMsg;
auto enrichedMsg = std::make_shared<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)));
[client processEnrichedMessage:enrichedMsg
handler:^(std::shared_ptr<EnrichedMessage> msg) {
dispatch_semaphore_signal(sema);
}];
[client processEnrichedMessage:enrichedMsg
handler:^(std::shared_ptr<EnrichedMessage> msg) {
dispatch_semaphore_signal(sema);
}];
XCTAssertEqual(0,
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
"Handler block not called within expected time window");
XCTAssertEqual(
0, dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
"Handler block not called within expected time window");
}
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testIsDatabasePath {
XCTAssertTrue([SNTEndpointSecurityClient isDatabasePath:"/private/var/db/santa/rules.db"]);
XCTAssertTrue([SNTEndpointSecurityClient isDatabasePath:"/private/var/db/santa/events.db"]);
- (void)testIsProtectedPath {
XCTAssertTrue([SNTEndpointSecurityClient isProtectedPath:"/private/var/db/santa/rules.db"]);
XCTAssertTrue([SNTEndpointSecurityClient isProtectedPath:"/private/var/db/santa/events.db"]);
XCTAssertFalse([SNTEndpointSecurityClient isDatabasePath:"/not/a/db/path"]);
XCTAssertFalse([SNTEndpointSecurityClient isProtectedPath:"/not/a/db/path"]);
}
- (void)testProcessMessageHandlerBadEventType {
@@ -316,7 +438,7 @@ using santa::santad::event_providers::endpoint_security::Message;
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
mockESApi->SetExpectationsRetainReleaseMessage();
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
@@ -345,7 +467,7 @@ using santa::santad::event_providers::endpoint_security::Message;
45 * 1000); // Long deadline to not hit
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
mockESApi->SetExpectationsRetainReleaseMessage();
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
@@ -359,11 +481,11 @@ using santa::santad::event_providers::endpoint_security::Message;
handler:^(const Message &msg) {
dispatch_semaphore_signal(sema);
}]);
}
XCTAssertEqual(0,
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
"Handler block not called within expected time window");
XCTAssertEqual(
0, dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
"Handler block not called within expected time window");
}
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
@@ -371,7 +493,7 @@ using santa::santad::event_providers::endpoint_security::Message;
- (void)testProcessMessageHandlerWithDeadlineTimeout {
// Set a es_message_t deadline of 750ms
// Set a deadline leeway in the `SNTEndpointSecurityClient` of 500ms
// Mock `RespondAuthResult` which is called from the deadline handler
// Mock `RespondFlagsResult` which is called from the deadline handler
// Signal the semaphore from the mock
// Wait a few seconds for the semaphore (should take ~250ms)
//
@@ -386,12 +508,12 @@ using santa::santad::event_providers::endpoint_security::Message;
750); // 750ms timeout
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
mockESApi->SetExpectationsRetainReleaseMessage();
dispatch_semaphore_t deadlineSema = dispatch_semaphore_create(0);
dispatch_semaphore_t controlSema = dispatch_semaphore_create(0);
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, ES_AUTH_RESULT_DENY, false))
EXPECT_CALL(*mockESApi, RespondFlagsResult(testing::_, testing::_, 0x0, false))
.WillOnce(testing::InvokeWithoutArgs(^() {
// Signal deadlineSema to let the handler block continue execution
dispatch_semaphore_signal(deadlineSema);

View File

@@ -237,6 +237,7 @@ NS_ASSUME_NONNULL_BEGIN
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];
@@ -247,10 +248,17 @@ NS_ASSUME_NONNULL_BEGIN
@"isEjectable: %d",
protocol, kind, isInternal, isRemovable, isEjectable);
// If the device is internal or virtual we are okay with the operation. We
// also are okay with operations for devices that are non-removal as long as
// they are NOT a USB device.
if (isInternal || isVirtual || (!isRemovable && !isEjectable && !isUSB)) {
// 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) {
return ES_AUTH_RESULT_ALLOW;
}

View File

@@ -124,8 +124,6 @@ class MockAuthResultCache : public AuthResultCache {
es_file_t file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&file);
es_message_t esMsg = MakeESMessage(eventType, &proc, ActionType::Auth, 6000);
// Need a pointer to esMsg to capture in blocks below.
es_message_t *heapESMsg = &esMsg;
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
@@ -142,7 +140,6 @@ class MockAuthResultCache : public AuthResultCache {
});
EXPECT_CALL(*mockESApi, RetainMessage).WillRepeatedly(^{
retainCount++;
return heapESMsg;
});
if (eventType == ES_EVENT_TYPE_AUTH_MOUNT) {
@@ -317,7 +314,7 @@ class MockAuthResultCache : public AuthResultCache {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsESNewClient();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
mockESApi->SetExpectationsRetainReleaseMessage();
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
EXPECT_CALL(*mockAuthCache, FlushCache);

View File

@@ -14,8 +14,10 @@
#import <Foundation/Foundation.h>
#include "Source/common/SNTCommon.h"
#include <string>
#include <vector>
#include "Source/santad/DataLayer/WatchItemPolicy.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/Metrics.h"
@@ -32,3 +34,22 @@
- (void)enable;
@end
// Extension of the `SNTEndpointSecurityEventHandler` protocol for
// `SNTEndpointSecurityClient` subclasses that can be dynamically
// enabled and disabled.
@protocol SNTEndpointSecurityDynamicEventHandler <SNTEndpointSecurityEventHandler>
// Called when a client should no longer receive events.
- (void)disable;
- (void)
watchItemsCount:(size_t)count
newPaths:
(const std::vector<std::pair<std::string, santa::santad::data_layer::WatchItemPathType>>
&)newPaths
removedPaths:
(const std::vector<std::pair<std::string, santa::santad::data_layer::WatchItemPathType>> &)
removedPaths;
@end

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