Compare commits

...

85 Commits

Author SHA1 Message Date
Russell Hancox
7502bc247f santactl/fileinfo: Include teamID/platform prefix in signing ID (#1356) 2024-05-21 12:48:48 -04:00
Russell Hancox
cf4dab55e0 santactl/rule: Allow adding signing ID and team ID rules by file path (#1357) 2024-05-21 12:48:27 -04:00
Matt W
e43ad30d4e Fix NSSecureCoding adoption in SNTFileAccessEvent (#1358) 2024-05-21 11:35:07 -04:00
np5
d8928ac320 Add cdhash, teamID, and signingID to bundle events (#1353)
Fix #1352
2024-05-20 10:45:36 -04:00
Matt W
ac1c9d8b05 Fix stat metrics accounting. Refactor setting metrics to be more general. (#1354) 2024-05-17 12:15:48 -04:00
Matt W
9b184ed4fb Add metric for when the file on disk is not the file being evaluated (#1348)
* Add metrics for stat change detection

* Fix test related issues due to partially constructed messages

* lint

* Convert errno to enum class StatResult

* Cleanup from PR feedback
2024-05-16 16:13:29 -04:00
Russell Hancox
67883c5200 GUI: Fix unicode rendering of attributed messages (#1351)
Also added a test to stop this from happening again
2024-05-15 16:27:28 -04:00
Russell Hancox
8e1e155c23 Project: Re-enable layering_checks (#1350) 2024-05-15 14:05:58 -04:00
Russell Hancox
fb6aa850b3 santad: Drop QoS of notify handling queue (#1349)
Bumping from BACKGROUND to DEFAULT had the desired impact of processing events faster and reducing memory usage but had a larger-than-expected increase in CPU usage. UTILITY is in the middle of these two and better fits the desired priority.
2024-05-15 11:53:31 -04:00
czcx
7f06b8c11a Docs: Minor grammar & correctness fixes in known-limitations.md. (#1345) 2024-05-14 13:06:45 -04:00
Matt W
978b33e450 Adopt --preserve-metadata flag to simplify resigning with entitlements (#1346) 2024-05-10 13:40:19 -04:00
Russell Hancox
f00ad32edd santad: Bump QoS of notify handling queue (#1342)
The use of the background queue is a historical artifact from when Santa had its own kernel extension with separate in-kernel queues for processing AUTH & NOTIFY type events. With the move to ES and the larger number of event types that we now notify on, running at the background QoS carries a small risk that the thread processing these events is not given a chance to run often enough that the queue grows and increases memory usage.
2024-05-09 15:44:14 -04:00
Pete Markowsky
7b0d2fdbb8 Add necessary dep for SNTPolicyProcessorTest (#1343) 2024-05-09 15:29:17 -04:00
Russell Hancox
1672e52b7b Project: Disable layering_check in all BUILD files (#1344) 2024-05-09 15:25:19 -04:00
Pete Markowsky
6cca5ab27d Update SNTPolicyProcessor to use a map (#1304)
* Update SNTPolicyProcessor to use a map instead of a giant switch statement

Update SNTPolicyProcessor to use a map instead of a giant switch statement.

Add unit tests for the method that sets SNTCachedDecision values.

* Remove unneccessary OCMock dep in BUILD file.

* Fix typo in method signature.

* Incorporate review feedback.

* Upper case UpdateCachedDecisionSigningInfo

* Update SNTPolicyProcessor.h

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>

* Update SNTPolicyProcessor.mm

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>

* Fix typo

* Fix linter issues.

* Fixed up more linter issues.

---------

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>
2024-05-09 14:38:12 -04:00
Matt W
7e4af5e337 Update to Abseil 20240116.1. Fix includes. (#1341) 2024-05-09 12:33:46 -04:00
Russell Hancox
5ea4431901 Project: Move fuzzing rules to bzlmod, fix santa_unit_test (#1339) 2024-05-08 11:50:04 -04:00
Tom Burgin
b53818f556 SNTBlockMessage: add more template options (#1337)
* update event detail url

* refactor template mappings

* re-enable testEventDetailURLForFileAccessEvent

* null

* missed one

* update comment
2024-05-07 09:20:50 -04:00
Tom Burgin
0f5e551345 Project: Fix lint.sh to bubble up all errors, switch from pylint to pyink, fix existing lint errors (#1338) 2024-05-06 16:47:32 -04:00
Nick Gregory
51b0f7146d Testing: update E2E to use JIT runners (#1335)
* inject jit runner token into e2e vm

* split out vm updating

* argparse + logging

* restore update/start vm steps

* pr review comments

* rm gcp.json and verify runner sha
2024-05-06 09:10:04 -04:00
czcx
f5882b3146 docs: Fix grammar and typo in syncing-overview 2024-04-30 12:58:34 -04:00
Rohan Sharma
59c146b4af README: Fix typo in landing page (#1332) 2024-04-30 12:25:53 -04:00
czcx
aaa2b0e259 Docs: Grammar updates on doc index 2024-04-29 17:23:39 -04:00
czcx
9c6fd0677f README: Minor grammar issue fix (#1329) 2024-04-29 15:53:52 -04:00
Russell Hancox
344a35aaf6 Project: Migrate to bazel modules (#1324)
This includes updating to rules_apple 3.5.1 and protobuf 26.1, as well as updating several tests to no longer use the data attribute to pass in testdata.
2024-04-11 17:19:30 -04:00
Matt W
45e36fa501 Bump protobuf to v26.1, update to use new interfaces. (#1317) 2024-04-11 14:22:43 -04:00
Nick Gregory
d5a7c5f1fa ProcessTree: add the first annotation, originator (4/4) (#1296) 2024-04-11 13:35:53 -04:00
Pete Markowsky
22aca6b505 Add macOS-14 to the test matrix. (#1323) 2024-04-05 15:09:04 -04:00
Pete Markowsky
375f7bd9cc Fix: Update code to use the new MOLCodesignChecker interfaces for codesigning info (#1322)
* Update code to use the  new MOLCodesignChecker interfaces for codesigning info.
2024-04-05 12:27:33 -04:00
Matt W
7d58665e87 Bump MOLCodesignChecker tag to latest (#1321) 2024-04-05 10:39:08 -04:00
Ryan Diers
3b2d02f38d GUI: Restore default button type to MessageWindow for blocked events (#1316) 2024-03-28 15:02:01 -04:00
Nick Gregory
57fc2b0253 Add missing EndpointSecurity dylib (#1315) 2024-03-25 13:41:20 -04:00
Nick Gregory
262adfecbd Fix BUILD deps (#1314) 2024-03-25 13:19:13 -04:00
Jason McCandless
1606657bb3 Add CDHash to rule evaluation order doc. (#1313) 2024-03-22 18:13:58 -04:00
Matt W
b379819cfa Overrides disabled when running tests unless explicitly enabled (#1312)
* Emit a log warning when overrides were applied

* Overrides now disabled in tests unless explicitly enabled

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

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

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

* Update enricher to read annotations from the ProcessTree

* rebase changes

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

* lingering build dep

* use tree factory constructor

* fix configurator

* build fixes

* rebase fixes

* fix tests

* review comments

* lint

* english hard

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

* Ensure hardened runtime for cdhash eval. Update docs.

* minor fixups

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

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

* Remove commented out code

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

* Handle older SDKs

* Formatting

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

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

* lingering darwin->macos

* lint

* remove defunct client id

* struct rename

* and one last header update

* use EndpointSecurityAPI in adapter

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

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

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

* FailClosed in Configurator now wraps checking client mode

* PR feedback

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

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

* case insensitive filesystems ahhhh

* tidy

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

* make Step implicitly called by Handle* methods

* lint

* naming convention

* widen pidversion to be generic

* move os specific backfill to os specific impl

* simplify ts checking

* retain/release a whole vec of pids

* document processtoken

* lint

* namespace

* add process tree to project-wide unit test target

* case change annotations

* case change annotations

* remove stray comment

* default initialize seen_timestamps

* fix missing initialization of refcnt and tombstoned

* reshuffle pb namespace

* pr review

* move annotation registration to tree construction

* use factory function for tree construction
2024-02-05 14:30:54 -05:00
Matt W
70474aba3e Sync clean all (#1275)
* WIP Clean syncs now leave non-transitive rules by default

* WIP Get existing tests compiling and passing

* Remove clean all sync server key. Basic tests.

* Add SNTConfiguratorTest, test deprecated key migration

* Revert changes to santactl status output

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

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

* Comment clean sync states. Test all permutations.

* Update docs for new sync keys

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

* Fix typo in SNTCommandRule.m.

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

* Use more consistent wording

* More consistent whitespace

* Reorder constants to appropriate section groups

* Update docs/deployment/configuration.md

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

---------

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

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

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

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

* Update metrics test for drop counts

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

* style

* comment typo
2023-12-05 18:51:07 -05:00
Matt W
2cbf15566a Revert "Project: Remove provisioning_profiles attributes from command-line tool rules (#1247)" (#1251)
This reverts commit 65c660298c.
2023-12-05 15:48:36 -05:00
Nick Gregory
1596990c65 reorder e2e tests (#1249) 2023-12-04 13:01:30 -05:00
Matt W
221664436f Expand debug logging for transitive rule failure case (#1248) 2023-11-30 15:47:48 -05:00
Russell Hancox
65c660298c Project: Remove provisioning_profiles attributes from command-line tool rules (#1247) 2023-11-30 13:50:38 -05:00
Matt W
2b5d55781c Revert back to C++17 for now (#1246) 2023-11-29 21:39:48 -05:00
Matt W
84e6d6ccff Fix USB state issue in santactl status (#1244) 2023-11-29 17:56:35 -05:00
Matt W
c16f90f5f9 Fix test issue caused by move to C++20 (#1245)
* Fix test issue caused by move to C++20

* Use spaceship operator as is the style of the time

* lint

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

* Handle code paths from santactl

* Don't bother evaluating isProdSignedCallback if not necessary

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

* Fix tests, old platform builds

* Use more recent availability checks

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

* Add EntitlementInfo proto message to store if entitlements were filtered

* Log cleanup

* Address PR feedback

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

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

* Pin workflow steps.
2023-11-09 15:26:51 -05:00
Travis Lane
ed227f43d4 Explicitly cast strings to std::string_view (#1230)
GoogleTest when built with GTEST_HAS_ABSL fails to convert these strings
to a `std::string_view`. Lets instead explicitly convert them to a
`std::string_view`.
2023-11-08 17:05:08 -05:00
Nick Gregory
056ed75bf1 dismiss santa popup after integration tests (#1226) 2023-11-07 14:42:03 -05:00
171 changed files with 7814 additions and 2359 deletions

View File

@@ -1 +1 @@
6.3.2
7.0.0

View File

@@ -9,6 +9,9 @@ jobs:
markdown-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: gaurav-nelson/github-action-markdown-link-check@v1
- run: "! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'"
- name: "Checkout Santa"
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # ratchet:actions/checkout@master
- name: "Check for deadlinks"
uses: gaurav-nelson/github-action-markdown-link-check@5c5dfc0ac2e225883c0e5f03a85311ec2830d368 # ratchet:gaurav-nelson/github-action-markdown-link-check@v1
- name: "Check for trailing whitespace and newlines"
run: "! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'"

View File

@@ -21,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-11, macos-12, macos-13]
os: [macos-11, macos-12, macos-13, macos-14]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
@@ -31,14 +31,14 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-11, macos-12, macos-13]
os: [macos-11, macos-12, macos-13, macos-14]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet: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
runs-on: macos-14
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
- name: Generate test coverage

View File

@@ -6,11 +6,29 @@ on:
workflow_dispatch:
jobs:
update_vm:
runs-on: e2e-host
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
- name: Update VM
env:
GCS_KEY: ${{ secrets.GCS_SERVICE_ACCOUNT_KEY }}
run: |
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp.json
echo "${GCS_KEY}" > ${GOOGLE_APPLICATION_CREDENTIALS}
function cleanup {
rm /tmp/gcp.json
}
trap cleanup EXIT
python3 Testing/integration/actions/update_vm.py macOS_14.bundle.tar.gz
start_vm:
runs-on: e2e-host
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
- name: Start VM
env:
RUNNER_REG_TOKEN: ${{ secrets.RUNNER_REG_TOKEN }}
run: python3 Testing/integration/actions/start_vm.py macOS_14.bundle.tar.gz
integration:
@@ -23,20 +41,23 @@ jobs:
run: echo "/opt/homebrew/bin/" >> $GITHUB_PATH
- name: Install configuration profile
run: bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig
- name: Build, install, and sync santa
run: |
bazel run :reload --define=SANTA_BUILD_TYPE=adhoc
bazel run //Testing/integration:allow_sysex
- name: Test config changes
run: ./Testing/integration/test_config_changes.sh
- name: Build, install, and start moroz
run: |
bazel build @com_github_groob_moroz//cmd/moroz:moroz
cp bazel-bin/external/com_github_groob_moroz/cmd/moroz/moroz_/moroz /tmp/moroz
/tmp/moroz -configs="$GITHUB_WORKSPACE/Testing/integration/configs/moroz_default/global.toml" -use-tls=false &
- name: Build, install, and sync santa
run: |
bazel run :reload --define=SANTA_BUILD_TYPE=adhoc
bazel run //Testing/integration:allow_sysex
sudo santactl sync --debug
- name: Run integration test binaries
run: bazel test //Testing/integration:integration_tests
- name: Test config changes
run: ./Testing/integration/test_config_changes.sh
run: |
bazel test //Testing/integration:integration_tests
sleep 3
bazel run //Testing/integration:dismiss_santa_popup || true
- name: Test sync server changes
run: ./Testing/integration/test_sync_changes.sh
- name: Test USB blocking

2
.gitignore vendored
View File

@@ -2,7 +2,7 @@
*.profraw
*.provisionprofile
bazel-*
Pods
MODULE.bazel.lock
Santa.xcodeproj/*
Santa.xcworkspace/*
CoverageData/*

5
.pyink-config Normal file
View File

@@ -0,0 +1,5 @@
[tool.pyink]
pyink = true
line-length = 80
pyink-indentation = 2
pyink-use-majority-quotes = true

429
.pylintrc
View File

@@ -1,429 +0,0 @@
# 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

4
BUILD
View File

@@ -1,7 +1,9 @@
load("@build_bazel_rules_apple//apple:versioning.bzl", "apple_bundle_version")
load("//:helper.bzl", "run_command")
package(default_visibility = ["//:santa_package_group"])
package(
default_visibility = ["//:santa_package_group"],
)
licenses(["notice"])

View File

@@ -53,11 +53,10 @@ readonly RELEASE_NAME="santa-$(/usr/bin/defaults read "${INPUT_APP}/Contents/Inf
readonly SCRATCH=$(/usr/bin/mktemp -d "${TMPDIR}/santa-"XXXXXX)
readonly APP_PKG_ROOT="${SCRATCH}/app_pkg_root"
readonly APP_PKG_SCRIPTS="${SCRATCH}/pkg_scripts"
readonly ENTITLEMENTS="${SCRATCH}/entitlements"
readonly SCRIPT_PATH="$(/usr/bin/dirname -- ${BASH_SOURCE[0]})"
/bin/mkdir -p "${APP_PKG_ROOT}" "${APP_PKG_SCRIPTS}" "${ENTITLEMENTS}"
/bin/mkdir -p "${APP_PKG_ROOT}" "${APP_PKG_SCRIPTS}"
readonly DMG_PATH="${ARTIFACTS_DIR}/${RELEASE_NAME}.dmg"
readonly TAR_PATH="${ARTIFACTS_DIR}/${RELEASE_NAME}.tar.gz"
@@ -65,19 +64,9 @@ readonly TAR_PATH="${ARTIFACTS_DIR}/${RELEASE_NAME}.tar.gz"
# Sign all of binaries/bundles. Maintain inside-out ordering where necessary
for ARTIFACT in "${INPUT_SANTACTL}" "${INPUT_SANTABS}" "${INPUT_SANTAMS}" "${INPUT_SANTASS}" "${INPUT_SYSX}" "${INPUT_APP}"; do
BN=$(/usr/bin/basename "${ARTIFACT}")
EN="${ENTITLEMENTS}/${BN}.entitlements"
echo "extracting ${BN} entitlements"
/usr/bin/codesign -d --entitlements "${EN}" "${ARTIFACT}"
if [[ -s "${EN}" ]]; then
EN="--entitlements ${EN}"
else
EN=""
fi
echo "codesigning ${BN}"
/usr/bin/codesign --sign "${SIGNING_IDENTITY}" --keychain "${SIGNING_KEYCHAIN}" \
${EN} --timestamp --force --generate-entitlement-der \
--preserve-metadata=entitlements --timestamp --force --generate-entitlement-der \
--options library,kill,runtime "${ARTIFACT}"
done

View File

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

55
MODULE.bazel Normal file
View File

@@ -0,0 +1,55 @@
module(name = "santa")
bazel_dep(name = "apple_support", version = "1.15.1", repo_name = "build_bazel_apple_support")
bazel_dep(name = "abseil-cpp", version = "20240116.1", repo_name = "com_google_absl")
bazel_dep(name = "rules_python", version = "0.31.0")
bazel_dep(name = "rules_cc", version = "0.0.9")
bazel_dep(name = "rules_apple", version = "3.5.0", repo_name = "build_bazel_rules_apple")
bazel_dep(name = "rules_swift", version = "1.18.0", repo_name = "build_bazel_rules_swift")
bazel_dep(name = "rules_fuzzing", version = "0.5.1")
bazel_dep(name = "protobuf", version = "main", repo_name = "com_google_protobuf")
git_override(
module_name = "protobuf",
commit = "21d75f861cdbc03b0a6b235a9ccf3ba0e1f09b32",
remote = "https://github.com/protocolbuffers/protobuf.git",
)
bazel_dep(name = "googletest", version = "1.14.0.bcr.1", repo_name = "com_google_googletest")
bazel_dep(name = "molcertificate", version = "2.1", repo_name = "MOLCertificate")
git_override(
module_name = "molcertificate",
commit = "34f0ccf68a34a07cc636ada89057c529f90bec3a",
remote = "https://github.com/google/macops-molcertificate.git",
)
bazel_dep(name = "molauthenticatingurlsession", version = "3.0", repo_name = "MOLAuthenticatingURLSession")
git_override(
module_name = "molauthenticatingurlsession",
commit = "0a50a67f29d635a4012981714c1dedef9ac25fe6",
remote = "https://github.com/google/macops-molauthenticatingurlsession.git",
)
bazel_dep(name = "molcodesignchecker", version = "3.0", repo_name = "MOLCodesignChecker")
git_override(
module_name = "molcodesignchecker",
commit = "5060bcc8baa90bae3b0ca705d14850328bbbec53",
remote = "https://github.com/google/macops-molcodesignchecker.git",
)
bazel_dep(name = "molxpcconnection", version = "2.1", repo_name = "MOLXPCConnection")
git_override(
module_name = "molxpcconnection",
commit = "da816dc49becac96d941ef6a5c4153ed39d1fe7c",
remote = "https://github.com/russellhancox/macops-molxpcconnection.git",
)
non_module_deps = use_extension("//:non_module_deps.bzl", "non_module_deps")
use_repo(non_module_deps, "FMDB")
use_repo(non_module_deps, "OCMock")
bazel_dep(name = "hedron_compile_commands", dev_dependency = True)
git_override(
module_name = "hedron_compile_commands",
commit = "0e990032f3c5a866e72615cf67e5ce22186dcb97",
remote = "https://github.com/hedronvision/bazel-compile-commands-extractor.git",
)

View File

@@ -21,7 +21,7 @@ It is named Santa because it keeps track of binaries that are naughty or nice.
# Docs
The Santa docs are stored in the
[Docs](https://github.com/google/santa/blob/main/docs) directory and published
[Docs](https://github.com/google/santa/blob/main/docs) directory and are published
at https://santa.dev.
The docs include deployment options, details on how parts of Santa work and

View File

@@ -11,6 +11,7 @@ proto_library(
name = "santa_proto",
srcs = ["santa.proto"],
deps = [
"//Source/santad/ProcessTree:process_tree_proto",
"@com_google_protobuf//:any_proto",
"@com_google_protobuf//:timestamp_proto",
],
@@ -40,6 +41,12 @@ objc_library(
],
)
objc_library(
name = "SNTDeepCopy",
srcs = ["SNTDeepCopy.m"],
hdrs = ["SNTDeepCopy.h"],
)
cc_library(
name = "SantaCache",
hdrs = ["SantaCache.h"],
@@ -54,6 +61,56 @@ santa_unit_test(
],
)
# This target shouldn't be used directly.
# Use a more specific scoped type instead.
objc_library(
name = "ScopedTypeRef",
hdrs = ["ScopedTypeRef.h"],
visibility = ["//Source/common:__pkg__"],
)
objc_library(
name = "ScopedCFTypeRef",
hdrs = ["ScopedCFTypeRef.h"],
deps = [
":ScopedTypeRef",
],
)
santa_unit_test(
name = "ScopedCFTypeRefTest",
srcs = ["ScopedCFTypeRefTest.mm"],
sdk_frameworks = [
"Security",
],
deps = [
":ScopedCFTypeRef",
],
)
objc_library(
name = "ScopedIOObjectRef",
hdrs = ["ScopedIOObjectRef.h"],
sdk_frameworks = [
"IOKit",
],
deps = [
":ScopedTypeRef",
],
)
santa_unit_test(
name = "ScopedIOObjectRefTest",
srcs = ["ScopedIOObjectRefTest.mm"],
sdk_frameworks = [
"IOKit",
],
deps = [
":ScopedIOObjectRef",
"//Source/santad:EndpointSecuritySerializerUtilities",
],
)
objc_library(
name = "BranchPrediction",
hdrs = ["BranchPrediction.h"],
@@ -153,7 +210,7 @@ objc_library(
],
deps = [
":CertificateHelpers",
"@MOLCertificate",
":SNTStoredEvent",
],
)
@@ -207,6 +264,7 @@ objc_library(
hdrs = ["SNTFileInfo.h"],
deps = [
":SNTLogging",
":SantaVnode",
"@FMDB",
"@MOLCodesignChecker",
],
@@ -256,6 +314,12 @@ santa_unit_test(
],
)
objc_library(
name = "SNTRuleIdentifiers",
srcs = ["SNTRuleIdentifiers.m"],
hdrs = ["SNTRuleIdentifiers.h"],
)
objc_library(
name = "SNTStoredEvent",
srcs = ["SNTStoredEvent.m"],
@@ -321,6 +385,7 @@ objc_library(
":SNTCommonEnums",
":SNTConfigurator",
":SNTRule",
":SNTRuleIdentifiers",
":SNTStoredEvent",
":SNTXPCUnprivilegedControlInterface",
"@MOLCodesignChecker",
@@ -363,6 +428,7 @@ objc_library(
deps = [
":SNTCommonEnums",
":SNTRule",
":SNTRuleIdentifiers",
":SNTStoredEvent",
":SNTXPCBundleServiceInterface",
":SantaVnode",
@@ -412,13 +478,26 @@ santa_unit_test(
name = "SNTBlockMessageTest",
srcs = ["SNTBlockMessageTest.m"],
deps = [
":SNTBlockMessage",
":SNTBlockMessage_SantaGUI",
":SNTConfigurator",
":SNTFileAccessEvent",
":SNTStoredEvent",
":SNTSystemInfo",
"@OCMock",
],
sdk_frameworks = [
"AppKit",
],
)
santa_unit_test(
name = "SNTConfiguratorTest",
srcs = ["SNTConfiguratorTest.m"],
deps = [
":SNTCommonEnums",
":SNTConfigurator",
"@OCMock",
],
)
test_suite(
@@ -427,11 +506,14 @@ test_suite(
":PrefixTreeTest",
":SNTBlockMessageTest",
":SNTCachedDecisionTest",
":SNTConfiguratorTest",
":SNTFileInfoTest",
":SNTKVOManagerTest",
":SNTMetricSetTest",
":SNTRuleTest",
":SantaCacheTest",
":ScopedCFTypeRefTest",
":ScopedIOObjectRefTest",
],
visibility = ["//:santa_package_group"],
)

View File

@@ -74,6 +74,11 @@ class PrefixTree {
node_count_ = 0;
}
uint32_t NodeCount() {
absl::ReaderMutexLock lock(&lock_);
return node_count_;
}
#if SANTA_PREFIX_TREE_DEBUG
void Print() {
char buf[max_depth_ + 1];
@@ -82,11 +87,6 @@ class PrefixTree {
absl::ReaderMutexLock lock(&lock_);
PrintLocked(root_, buf, 0);
}
uint32_t NodeCount() {
absl::ReaderMutexLock lock(&lock_);
return node_count_;
}
#endif
private:

View File

@@ -20,6 +20,10 @@
#import "Source/common/SNTStoredEvent.h"
#import "Source/common/SNTSystemInfo.h"
static id ValueOrNull(id value) {
return value ?: [NSNull null];
}
@implementation SNTBlockMessage
+ (NSAttributedString *)formatMessage:(NSString *)message {
@@ -52,7 +56,11 @@
#ifdef SANTAGUI
NSData *htmlData = [fullHTML dataUsingEncoding:NSUTF8StringEncoding];
return [[NSAttributedString alloc] initWithHTML:htmlData documentAttributes:NULL];
NSDictionary *options = @{
NSDocumentTypeDocumentAttribute : NSHTMLTextDocumentType,
NSCharacterEncodingDocumentAttribute : @(NSUTF8StringEncoding),
};
return [[NSAttributedString alloc] initWithHTML:htmlData options:options documentAttributes:NULL];
#else
NSString *strippedHTML = [self stringFromHTML:fullHTML];
if (!strippedHTML) {
@@ -123,35 +131,79 @@
}
+ (NSString *)replaceFormatString:(NSString *)str
withDict:(NSDictionary<NSString *, NSString * (^)()> *)replacements {
withDict:(NSDictionary<NSString *, NSString *> *)replacements {
__block NSString *formatStr = str;
[replacements
enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString * (^computeValue)(), BOOL *stop) {
NSString *value = computeValue();
if (value) {
formatStr = [formatStr stringByReplacingOccurrencesOfString:key withString:value];
}
}];
[replacements enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) {
if ((id)value != [NSNull null]) {
formatStr = [formatStr stringByReplacingOccurrencesOfString:key withString:value];
}
}];
return formatStr;
}
// Returns either the generated URL for the passed in event, or an NSURL from the passed in custom
// URL string. If the custom URL string is the string "null", nil will be returned. If no custom
// URL is passed and there is no configured EventDetailURL template, nil will be returned.
// The following "format strings" will be replaced in the URL, if they are present:
//
// The following "format strings" will be replaced in the URL provided by
// `+eventDetailURLForEvent:customURL:templateMapping:`.
//
// %file_identifier% - The SHA-256 of the binary being executed.
// %bundle_or_file_identifier% - The hash of the bundle containing this file or the file itself,
// if no bundle hash is present.
// %file_bundle_id% - The bundle id of the binary, if any.
// %team_id% - The Team ID if present in the signature information.
// %signing_id% - The Signing ID if present in the signature information.
// %cdhash% - If signed, the CDHash.
// %username% - The executing user's name.
// %machine_id% - The configured machine ID for this host.
// %hostname% - The machine's FQDN.
// %uuid% - The machine's UUID.
// %serial% - The machine's serial number.
//
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event customURL:(NSString *)url {
+ (NSDictionary *)eventDetailTemplateMappingForEvent:(SNTStoredEvent *)event {
SNTConfigurator *config = [SNTConfigurator configurator];
return @{
@"%file_sha%" : ValueOrNull(event.fileSHA256 ? event.fileBundleHash ?: event.fileSHA256 : nil),
@"%file_identifier%" : ValueOrNull(event.fileSHA256),
@"%bundle_or_file_identifier%" :
ValueOrNull(event.fileSHA256 ? event.fileBundleHash ?: event.fileSHA256 : nil),
@"%username%" : ValueOrNull(event.executingUser),
@"%file_bundle_id%" : ValueOrNull(event.fileBundleID),
@"%team_id%" : ValueOrNull(event.teamID),
@"%signing_id%" : ValueOrNull(event.signingID),
@"%cdhash%" : ValueOrNull(event.cdhash),
@"%machine_id%" : ValueOrNull(config.machineID),
@"%hostname%" : ValueOrNull([SNTSystemInfo longHostname]),
@"%uuid%" : ValueOrNull([SNTSystemInfo hardwareUUID]),
@"%serial%" : ValueOrNull([SNTSystemInfo serialNumber]),
};
}
//
// Everything from `+eventDetailTemplateMappingForEvent:` with the following file access
// specific templates.
//
// %rule_version% - The version of the rule that was violated.
// %rule_name% - The name of the rule that was violated.
// %accessed_path% - The path accessed by the binary.
//
+ (NSDictionary *)fileAccessEventDetailTemplateMappingForEvent:(SNTFileAccessEvent *)event {
NSMutableDictionary *d = [self eventDetailTemplateMappingForEvent:event].mutableCopy;
[d addEntriesFromDictionary:@{
@"%rule_version%" : ValueOrNull(event.ruleVersion),
@"%rule_name%" : ValueOrNull(event.ruleName),
@"%accessed_path%" : ValueOrNull(event.accessedPath),
}];
return d;
}
// Returns either the generated URL for the passed in event, or an NSURL from the passed in custom
// URL string. If the custom URL string is the string "null", nil will be returned. If no custom
// URL is passed and there is no configured EventDetailURL template, nil will be returned.
// The "format strings" in `templateMapping` will be replaced in the URL, if they are present.
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event
customURL:(NSString *)url
templateMapping:(NSDictionary *)templateMapping {
SNTConfigurator *config = [SNTConfigurator configurator];
NSString *formatStr = url;
@@ -166,26 +218,7 @@
return nil;
}
// Disabling clang-format. See comment in `eventDetailURLForFileAccessEvent:customURL:`
// clang-format off
NSDictionary<NSString *, NSString * (^)()> *kvReplacements =
[NSDictionary dictionaryWithObjectsAndKeys:
// This key is deprecated, use %file_identifier% or %bundle_or_file_identifier%
^{ return event.fileSHA256 ? event.fileBundleHash ?: event.fileSHA256 : nil; },
@"%file_sha%",
^{ return event.fileSHA256; }, @"%file_identifier%",
^{ return event.fileSHA256 ? event.fileBundleHash ?: event.fileSHA256 : nil; },
@"%bundle_or_file_identifier%",
^{ return event.executingUser; }, @"%username%",
^{ return config.machineID; }, @"%machine_id%",
^{ return [SNTSystemInfo longHostname]; }, @"%hostname%",
^{ return [SNTSystemInfo hardwareUUID]; }, @"%uuid%",
^{ return [SNTSystemInfo serialNumber]; }, @"%serial%",
nil];
// clang-format on
formatStr = [SNTBlockMessage replaceFormatString:formatStr withDict:kvReplacements];
formatStr = [SNTBlockMessage replaceFormatString:formatStr withDict:templateMapping];
NSURL *u = [NSURL URLWithString:formatStr];
if (!u) {
LOGW(@"Unable to generate event detail URL for string '%@'", formatStr);
@@ -194,55 +227,16 @@
return u;
}
// Returns either the generated URL for the passed in event, or an NSURL from the passed in custom
// URL string. If the custom URL string is the string "null", nil will be returned. If no custom
// URL is passed and there is no configured EventDetailURL template, nil will be returned.
// The following "format strings" will be replaced in the URL, if they are present:
//
// %rule_version% - The version of the rule that was violated.
// %rule_name% - The name of the rule that was violated.
// %file_identifier% - The SHA-256 of the binary being executed.
// %accessed_path% - The path accessed by the binary.
// %username% - The executing user's name.
// %machine_id% - The configured machine ID for this host.
// %hostname% - The machine's FQDN.
// %uuid% - The machine's UUID.
// %serial% - The machine's serial number.
//
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event customURL:(NSString *)url {
return [self eventDetailURLForEvent:event
customURL:url
templateMapping:[self eventDetailTemplateMappingForEvent:event]];
}
+ (NSURL *)eventDetailURLForFileAccessEvent:(SNTFileAccessEvent *)event customURL:(NSString *)url {
if (!url.length || [url isEqualToString:@"null"]) {
return nil;
}
SNTConfigurator *config = [SNTConfigurator configurator];
// Clang format goes wild here. If you use the container literal syntax `@{}` with a block value
// type, it seems to break the clang format on/off functionality and breaks formatting for the
// remainder of the file.
// Using `dictionaryWithObjectsAndKeys` and disabling clang format as a workaround.
// clang-format off
NSDictionary<NSString *, NSString * (^)()> *kvReplacements =
[NSDictionary dictionaryWithObjectsAndKeys:
^{ return event.ruleVersion; }, @"%rule_version%",
^{ return event.ruleName; }, @"%rule_name%",
^{ return event.fileSHA256; }, @"%file_identifier%",
^{ return event.accessedPath; }, @"%accessed_path%",
^{ return event.executingUser; }, @"%username%",
^{ return config.machineID; }, @"%machine_id%",
^{ return [SNTSystemInfo longHostname]; }, @"%hostname%",
^{ return [SNTSystemInfo hardwareUUID]; }, @"%uuid%",
^{ return [SNTSystemInfo serialNumber]; }, @"%serial%",
nil];
// clang-format on
NSString *formatStr = [SNTBlockMessage replaceFormatString:url withDict:kvReplacements];
NSURL *u = [NSURL URLWithString:formatStr];
if (!u) {
LOGW(@"Unable to generate event detail URL for string '%@'", formatStr);
}
return u;
return [self eventDetailURLForEvent:event
customURL:url
templateMapping:[self fileAccessEventDetailTemplateMappingForEvent:event]];
}
@end

View File

@@ -39,18 +39,30 @@
OCMStub([self.mockSystemInfo serialNumber]).andReturn(@"my_s");
}
- (void)testFormatMessage {
NSString *input = @"Testing with somé Ünicode çharacters";
NSAttributedString *got = [SNTBlockMessage formatMessage:input];
XCTAssertEqualObjects([got string], input);
}
- (void)testEventDetailURLForEvent {
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
se.fileSHA256 = @"my_fi";
se.executingUser = @"my_un";
se.fileBundleID = @"s.n.t";
se.cdhash = @"abc";
se.teamID = @"SNT";
se.signingID = @"SNT:s.n.t";
NSString *url = @"http://"
@"localhost?fs=%file_sha%&fi=%file_identifier%&bfi=%bundle_or_file_identifier%&"
@"fbid=%file_bundle_id%&ti=%team_id%&si=%signing_id%&ch=%cdhash%&"
@"un=%username%&mid=%machine_id%&hn=%hostname%&u=%uuid%&s=%serial%";
NSString *wantUrl =
@"http://"
@"localhost?fs=my_fi&fi=my_fi&bfi=my_fi&bfi=my_fi&un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
NSString *wantUrl = @"http://"
@"localhost?fs=my_fi&fi=my_fi&bfi=my_fi&"
@"fbid=s.n.t&ti=SNT&si=SNT:s.n.t&ch=abc&"
@"un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
NSURL *gotUrl = [SNTBlockMessage eventDetailURLForEvent:se customURL:url];
@@ -58,7 +70,9 @@
se.fileBundleHash = @"my_fbh";
wantUrl = @"http://"
@"localhost?fs=my_fbh&fi=my_fi&bfi=my_fbh&un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
@"localhost?fs=my_fbh&fi=my_fi&bfi=my_fbh&"
@"fbid=s.n.t&ti=SNT&si=SNT:s.n.t&ch=abc&"
@"un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
gotUrl = [SNTBlockMessage eventDetailURLForEvent:se customURL:url];
@@ -74,15 +88,22 @@
fae.ruleVersion = @"my_rv";
fae.ruleName = @"my_rn";
fae.fileSHA256 = @"my_fi";
fae.fileBundleID = @"s.n.t";
fae.cdhash = @"abc";
fae.teamID = @"SNT";
fae.signingID = @"SNT:s.n.t";
fae.accessedPath = @"my_ap";
fae.executingUser = @"my_un";
NSString *url = @"http://"
@"localhost?rv=%rule_version%&rn=%rule_name%&fi=%file_identifier%&ap=%accessed_"
@"path%&un=%username%&mid=%machine_id%&hn=%hostname%&u=%uuid%&s=%serial%";
NSString *wantUrl =
NSString *url =
@"http://"
@"localhost?rv=my_rv&rn=my_rn&fi=my_fi&ap=my_ap&un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
@"localhost?rv=%rule_version%&rn=%rule_name%&fi=%file_identifier%&"
@"fbid=%file_bundle_id%&ti=%team_id%&si=%signing_id%&ch=%cdhash%&"
@"ap=%accessed_path%&un=%username%&mid=%machine_id%&hn=%hostname%&u=%uuid%&s=%serial%";
NSString *wantUrl = @"http://"
@"localhost?rv=my_rv&rn=my_rn&fi=my_fi&"
@"fbid=s.n.t&ti=SNT&si=SNT:s.n.t&ch=abc&"
@"ap=my_ap&un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
NSURL *gotUrl = [SNTBlockMessage eventDetailURLForFileAccessEvent:fae customURL:url];
@@ -92,4 +113,17 @@
XCTAssertNil([SNTBlockMessage eventDetailURLForFileAccessEvent:fae customURL:@"null"]);
}
- (void)testEventDetailURLMissingDetails {
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
se.fileSHA256 = @"my_fi";
NSString *url = @"http://localhost?fi=%file_identifier%";
NSString *wantUrl = @"http://localhost?fi=my_fi";
NSURL *gotUrl = [SNTBlockMessage eventDetailURLForEvent:se customURL:url];
XCTAssertEqualObjects(gotUrl.absoluteString, wantUrl);
}
@end

View File

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

View File

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

View File

@@ -46,6 +46,7 @@ typedef NS_ENUM(NSInteger, SNTAction) {
typedef NS_ENUM(NSInteger, SNTRuleType) {
SNTRuleTypeUnknown = 0,
SNTRuleTypeCDHash = 500,
SNTRuleTypeBinary = 1000,
SNTRuleTypeSigningID = 2000,
SNTRuleTypeCertificate = 3000,
@@ -84,6 +85,7 @@ typedef NS_ENUM(uint64_t, SNTEventState) {
SNTEventStateBlockTeamID = 1ULL << 20,
SNTEventStateBlockLongPath = 1ULL << 21,
SNTEventStateBlockSigningID = 1ULL << 22,
SNTEventStateBlockCDHash = 1ULL << 23,
// Bits 40-63 store allow decision types
SNTEventStateAllowUnknown = 1ULL << 40,
@@ -95,6 +97,7 @@ typedef NS_ENUM(uint64_t, SNTEventState) {
SNTEventStateAllowPendingTransitive = 1ULL << 46,
SNTEventStateAllowTeamID = 1ULL << 47,
SNTEventStateAllowSigningID = 1ULL << 48,
SNTEventStateAllowCDHash = 1ULL << 49,
// Block and Allow masks
SNTEventStateBlock = 0xFFFFFFULL << 16,
@@ -166,7 +169,20 @@ typedef NS_ENUM(NSInteger, SNTDeviceManagerStartupPreferences) {
SNTDeviceManagerStartupPreferencesForceRemount,
};
typedef NS_ENUM(NSInteger, SNTSyncType) {
SNTSyncTypeNormal,
SNTSyncTypeClean,
SNTSyncTypeCleanAll,
};
typedef NS_ENUM(NSInteger, SNTRuleCleanup) {
SNTRuleCleanupNone,
SNTRuleCleanupAll,
SNTRuleCleanupNonTransitive,
};
#ifdef __cplusplus
enum class FileAccessPolicyDecision {
kNoPolicy,
kDenied,
@@ -175,6 +191,19 @@ enum class FileAccessPolicyDecision {
kAllowedReadAccess,
kAllowedAuditOnly,
};
enum class StatChangeStep {
kNoChange = 0,
kMessageCreate,
kCodesignValidation,
};
enum class StatResult {
kOK = 0,
kStatError,
kDevnoInodeMismatch,
};
#endif
static const char *kSantaDPath =

View File

@@ -40,7 +40,8 @@
///
/// Enable Fail Close mode. Defaults to NO.
/// This controls Santa's behavior when a failure occurs, such as an
/// inability to read a file. By default, to prevent bugs or misconfiguration
/// inability to read a file and as a default response when deadlines
/// are about to expire. By default, to prevent bugs or misconfiguration
/// from rendering a machine inoperable Santa will fail open and allow
/// execution. With this setting enabled, Santa will fail closed if the client
/// is in LOCKDOWN mode, offering a higher level of security but with a higher
@@ -284,7 +285,7 @@
///
/// Enabling this appends the Santa machine ID to the end of each log line. If nothing
/// has been overriden, this is the host's UUID.
/// has been overridden, this is the host's UUID.
/// Defaults to NO.
///
@property(readonly, nonatomic) BOOL enableMachineIDDecoration;
@@ -437,9 +438,9 @@
@property(nonatomic) NSDate *ruleSyncLastSuccess;
///
/// If YES a clean sync is required.
/// Type of sync required (e.g. normal, clean, etc.).
///
@property(nonatomic) BOOL syncCleanRequired;
@property(nonatomic) SNTSyncType syncTypeRequired;
#pragma mark - USB Settings
@@ -449,7 +450,7 @@
@property(nonatomic) BOOL blockUSBMount;
///
/// Comma-seperated `$ mount -o` arguments used for forced remounting of USB devices. Default
/// Comma-separated `$ mount -o` arguments used for forced remounting of USB devices. Default
/// to fully allow/deny without remounting if unset.
///
@property(nonatomic) NSArray<NSString *> *remountUSBMode;
@@ -642,6 +643,24 @@
///
@property(readonly, nonatomic) NSUInteger metricExportTimeout;
///
/// List of prefix strings for which individual entitlement keys with a matching
/// prefix should not be logged.
///
@property(readonly, nonatomic) NSArray<NSString *> *entitlementsPrefixFilter;
///
/// List of TeamIDs for which entitlements should not be logged. Use the string
/// "platform" to refer to platform binaries.
///
@property(readonly, nonatomic) NSArray<NSString *> *entitlementsTeamIDFilter;
///
/// List of enabled process annotations.
/// This property is not KVO compliant.
///
@property(readonly, nonatomic) NSArray<NSString *> *enabledProcessAnnotations;
///
/// Retrieve an initialized singleton configurator object using the default file path.
///

View File

@@ -20,6 +20,21 @@
#import "Source/common/SNTStrengthify.h"
#import "Source/common/SNTSystemInfo.h"
// Ensures the given object is an NSArray and only contains NSString value types
static NSArray<NSString *> *EnsureArrayOfStrings(id obj) {
if (![obj isKindOfClass:[NSArray class]]) {
return nil;
}
for (id item in obj) {
if (![item isKindOfClass:[NSString class]]) {
return nil;
}
}
return obj;
}
@interface SNTConfigurator ()
/// A NSUserDefaults object set to use the com.google.santa suite.
@property(readonly, nonatomic) NSUserDefaults *defaults;
@@ -38,6 +53,9 @@
/// Holds the last processed hash of the static rules list.
@property(atomic) NSDictionary *cachedStaticRules;
@property(readonly, nonatomic) NSString *syncStateFilePath;
@property(nonatomic, copy) BOOL (^syncStateAccessAuthorizerBlock)();
@end
@implementation SNTConfigurator
@@ -88,6 +106,8 @@ static NSString *const kModeNotificationLockdown = @"ModeNotificationLockdown";
static NSString *const kEnablePageZeroProtectionKey = @"EnablePageZeroProtection";
static NSString *const kEnableBadSignatureProtectionKey = @"EnableBadSignatureProtection";
static NSString *const kFailClosedKey = @"FailClosed";
static NSString *const kDisableUnknownEventUploadKey = @"DisableUnknownEventUpload";
static NSString *const kFileChangesRegexKey = @"FileChangesRegex";
static NSString *const kFileChangesPrefixFiltersKey = @"FileChangesPrefixFilters";
@@ -116,21 +136,10 @@ static NSString *const kFCMProject = @"FCMProject";
static NSString *const kFCMEntity = @"FCMEntity";
static NSString *const kFCMAPIKey = @"FCMAPIKey";
// The keys managed by a sync server or mobileconfig.
static NSString *const kClientModeKey = @"ClientMode";
static NSString *const kFailClosedKey = @"FailClosed";
static NSString *const kBlockUSBMountKey = @"BlockUSBMount";
static NSString *const kRemountUSBModeKey = @"RemountUSBMode";
static NSString *const kEntitlementsPrefixFilterKey = @"EntitlementsPrefixFilter";
static NSString *const kEntitlementsTeamIDFilterKey = @"EntitlementsTeamIDFilter";
static NSString *const kOnStartUSBOptions = @"OnStartUSBOptions";
static NSString *const kEnableTransitiveRulesKey = @"EnableTransitiveRules";
static NSString *const kEnableTransitiveRulesKeyDeprecated = @"EnableTransitiveWhitelisting";
static NSString *const kAllowedPathRegexKey = @"AllowedPathRegex";
static NSString *const kAllowedPathRegexKeyDeprecated = @"WhitelistRegex";
static NSString *const kBlockedPathRegexKey = @"BlockedPathRegex";
static NSString *const kBlockedPathRegexKeyDeprecated = @"BlacklistRegex";
static NSString *const kEnableAllEventUploadKey = @"EnableAllEventUpload";
static NSString *const kDisableUnknownEventUploadKey = @"DisableUnknownEventUpload";
static NSString *const kOverrideFileAccessActionKey = @"OverrideFileAccessAction";
static NSString *const kMetricFormat = @"MetricFormat";
static NSString *const kMetricURL = @"MetricURL";
@@ -138,12 +147,37 @@ static NSString *const kMetricExportInterval = @"MetricExportInterval";
static NSString *const kMetricExportTimeout = @"MetricExportTimeout";
static NSString *const kMetricExtraLabels = @"MetricExtraLabels";
static NSString *const kEnabledProcessAnnotations = @"EnabledProcessAnnotations";
// The keys managed by a sync server or mobileconfig.
static NSString *const kClientModeKey = @"ClientMode";
static NSString *const kBlockUSBMountKey = @"BlockUSBMount";
static NSString *const kRemountUSBModeKey = @"RemountUSBMode";
static NSString *const kEnableTransitiveRulesKey = @"EnableTransitiveRules";
static NSString *const kEnableTransitiveRulesKeyDeprecated = @"EnableTransitiveWhitelisting";
static NSString *const kAllowedPathRegexKey = @"AllowedPathRegex";
static NSString *const kAllowedPathRegexKeyDeprecated = @"WhitelistRegex";
static NSString *const kBlockedPathRegexKey = @"BlockedPathRegex";
static NSString *const kBlockedPathRegexKeyDeprecated = @"BlacklistRegex";
static NSString *const kEnableAllEventUploadKey = @"EnableAllEventUpload";
static NSString *const kOverrideFileAccessActionKey = @"OverrideFileAccessAction";
// The keys managed by a sync server.
static NSString *const kFullSyncLastSuccess = @"FullSyncLastSuccess";
static NSString *const kRuleSyncLastSuccess = @"RuleSyncLastSuccess";
static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
static NSString *const kSyncCleanRequiredDeprecated = @"SyncCleanRequired";
static NSString *const kSyncTypeRequired = @"SyncTypeRequired";
- (instancetype)init {
return [self initWithSyncStateFile:kSyncStateFilePath
syncStateAccessAuthorizer:^BOOL() {
// Only access the sync state if a sync server is configured and running as root
return self.syncBaseURL != nil && geteuid() == 0;
}];
}
- (instancetype)initWithSyncStateFile:(NSString *)syncStateFilePath
syncStateAccessAuthorizer:(BOOL (^)(void))syncStateAccessAuthorizer {
self = [super init];
if (self) {
Class number = [NSNumber class];
@@ -165,7 +199,8 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kRemountUSBModeKey : array,
kFullSyncLastSuccess : date,
kRuleSyncLastSuccess : date,
kSyncCleanRequired : number,
kSyncCleanRequiredDeprecated : number,
kSyncTypeRequired : number,
kEnableAllEventUploadKey : number,
kOverrideFileAccessActionKey : string,
};
@@ -240,12 +275,25 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kEnableAllEventUploadKey : number,
kDisableUnknownEventUploadKey : number,
kOverrideFileAccessActionKey : string,
kEntitlementsPrefixFilterKey : array,
kEntitlementsTeamIDFilterKey : array,
kEnabledProcessAnnotations : array,
};
_syncStateFilePath = syncStateFilePath;
_syncStateAccessAuthorizerBlock = syncStateAccessAuthorizer;
_defaults = [NSUserDefaults standardUserDefaults];
[_defaults addSuiteNamed:@"com.google.santa"];
_configState = [self readForcedConfig];
[self cacheStaticRules];
_syncState = [self readSyncStateFromDisk] ?: [NSMutableDictionary dictionary];
if ([self migrateDeprecatedSyncStateKeys]) {
// Save the updated sync state if any keys were migrated.
[self saveSyncStateToDisk];
}
_debugFlag = [[NSProcessInfo processInfo].arguments containsObject:@"--debug"];
[self startWatchingDefaults];
}
@@ -411,7 +459,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self syncStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingSyncCleanRequired {
+ (NSSet *)keyPathsForValuesAffectingSyncTypeRequired {
return [self syncStateSet];
}
@@ -527,6 +575,14 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self syncAndConfigStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingEntitlementsPrefixFilter {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingEntitlementsTeamIDFilter {
return [self configStateSet];
}
#pragma mark Public Interface
- (SNTClientMode)clientMode {
@@ -551,8 +607,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
- (BOOL)failClosed {
NSNumber *n = self.configState[kFailClosedKey];
if (n) return [n boolValue];
return NO;
return [n boolValue] && self.clientMode == SNTClientModeLockdown;
}
- (BOOL)enableTransitiveRules {
@@ -794,12 +849,12 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
[self updateSyncStateForKey:kRuleSyncLastSuccess value:ruleSyncLastSuccess];
}
- (BOOL)syncCleanRequired {
return [self.syncState[kSyncCleanRequired] boolValue];
- (SNTSyncType)syncTypeRequired {
return (SNTSyncType)[self.syncState[kSyncTypeRequired] integerValue];
}
- (void)setSyncCleanRequired:(BOOL)syncCleanRequired {
[self updateSyncStateForKey:kSyncCleanRequired value:@(syncCleanRequired)];
- (void)setSyncTypeRequired:(SNTSyncType)syncTypeRequired {
[self updateSyncStateForKey:kSyncTypeRequired value:@(syncTypeRequired)];
}
- (NSString *)machineOwner {
@@ -1053,6 +1108,16 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return self.configState[kMetricExtraLabels];
}
- (NSArray<NSString *> *)enabledProcessAnnotations {
NSArray<NSString *> *annotations = self.configState[kEnabledProcessAnnotations];
for (id annotation in annotations) {
if (![annotation isKindOfClass:[NSString class]]) {
return nil;
}
}
return annotations;
}
#pragma mark Private
///
@@ -1071,12 +1136,12 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
/// Read the saved syncState.
///
- (NSMutableDictionary *)readSyncStateFromDisk {
// Only read the sync state if a sync server is configured.
if (!self.syncBaseURL) return nil;
// Only santad should read this file.
if (geteuid() != 0) return nil;
if (!self.syncStateAccessAuthorizerBlock()) {
return nil;
}
NSMutableDictionary *syncState =
[NSMutableDictionary dictionaryWithContentsOfFile:kSyncStateFilePath];
[NSMutableDictionary dictionaryWithContentsOfFile:self.syncStateFilePath];
for (NSString *key in syncState.allKeys) {
if (self.syncServerKeyTypes[key] == [NSRegularExpression class]) {
NSString *pattern = [syncState[key] isKindOfClass:[NSString class]] ? syncState[key] : nil;
@@ -1086,24 +1151,54 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
continue;
}
}
return syncState;
}
///
/// Migrate any deprecated sync state keys/values to alternative keys/values.
///
/// Returns YES if any keys were migrated. Otherwise NO.
///
- (BOOL)migrateDeprecatedSyncStateKeys {
// Currently only one key to migrate
if (!self.syncState[kSyncCleanRequiredDeprecated]) {
return NO;
}
NSMutableDictionary *syncState = self.syncState.mutableCopy;
// If the kSyncTypeRequired key exists, its current value will take precedence.
// Otherwise, migrate the old value to be compatible with the new logic.
if (!self.syncState[kSyncTypeRequired]) {
syncState[kSyncTypeRequired] = [self.syncState[kSyncCleanRequiredDeprecated] boolValue]
? @(SNTSyncTypeClean)
: @(SNTSyncTypeNormal);
}
// Delete the deprecated key
syncState[kSyncCleanRequiredDeprecated] = nil;
self.syncState = syncState;
return YES;
}
///
/// Saves the current effective syncState to disk.
///
- (void)saveSyncStateToDisk {
// Only save the sync state if a sync server is configured.
if (!self.syncBaseURL) return;
// Only santad should write to this file.
if (geteuid() != 0) return;
if (!self.syncStateAccessAuthorizerBlock()) {
return;
}
// Either remove
NSMutableDictionary *syncState = self.syncState.mutableCopy;
syncState[kAllowedPathRegexKey] = [syncState[kAllowedPathRegexKey] pattern];
syncState[kBlockedPathRegexKey] = [syncState[kBlockedPathRegexKey] pattern];
[syncState writeToFile:kSyncStateFilePath atomically:YES];
[syncState writeToFile:self.syncStateFilePath atomically:YES];
[[NSFileManager defaultManager] setAttributes:@{NSFilePosixPermissions : @0600}
ofItemAtPath:kSyncStateFilePath
ofItemAtPath:self.syncStateFilePath
error:NULL];
}
@@ -1111,6 +1206,14 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
self.syncState = [NSMutableDictionary dictionary];
}
- (NSArray *)entitlementsPrefixFilter {
return EnsureArrayOfStrings(self.configState[kEntitlementsPrefixFilterKey]);
}
- (NSArray *)entitlementsTeamIDFilter {
return EnsureArrayOfStrings(self.configState[kEntitlementsTeamIDFilterKey]);
}
#pragma mark Private Defaults Methods
- (NSRegularExpression *)expressionForPattern:(NSString *)pattern {
@@ -1119,6 +1222,39 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:NULL];
}
- (void)applyOverrides:(NSMutableDictionary *)forcedConfig {
// Overrides should only be applied under debug builds.
#ifdef DEBUG
if ([[[NSProcessInfo processInfo] processName] isEqualToString:@"xctest"] &&
![[[NSProcessInfo processInfo] environment] objectForKey:@"ENABLE_CONFIG_OVERRIDES"]) {
// By default, config overrides are not applied when running tests to help
// mitigate potential issues due to unexpected config values. This behavior
// can be overriden if desired by using the env variable: `ENABLE_CONFIG_OVERRIDES`.
//
// E.g.:
// bazel test --test_env=ENABLE_CONFIG_OVERRIDES=1 ...other test args...
return;
}
NSDictionary *overrides = [NSDictionary dictionaryWithContentsOfFile:kConfigOverrideFilePath];
for (NSString *key in overrides) {
id obj = overrides[key];
if (![obj isKindOfClass:self.forcedConfigKeyTypes[key]] ||
([self.forcedConfigKeyTypes[key] isKindOfClass:[NSRegularExpression class]] &&
![obj isKindOfClass:[NSString class]])) {
continue;
}
forcedConfig[key] = obj;
if (self.forcedConfigKeyTypes[key] == [NSRegularExpression class]) {
NSString *pattern = [obj isKindOfClass:[NSString class]] ? obj : nil;
forcedConfig[key] = [self expressionForPattern:pattern];
}
}
#endif
}
- (NSMutableDictionary *)readForcedConfig {
NSMutableDictionary *forcedConfig = [NSMutableDictionary dictionary];
for (NSString *key in self.forcedConfigKeyTypes) {
@@ -1130,22 +1266,9 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
forcedConfig[key] = [self expressionForPattern:pattern];
}
}
#ifdef DEBUG
NSDictionary *overrides = [NSDictionary dictionaryWithContentsOfFile:kConfigOverrideFilePath];
for (NSString *key in overrides) {
id obj = overrides[key];
if (![obj isKindOfClass:self.forcedConfigKeyTypes[key]] ||
([self.forcedConfigKeyTypes[key] isKindOfClass:[NSRegularExpression class]] &&
![obj isKindOfClass:[NSString class]])) {
continue;
}
forcedConfig[key] = obj;
if (self.forcedConfigKeyTypes[key] == [NSRegularExpression class]) {
NSString *pattern = [obj isKindOfClass:[NSString class]] ? obj : nil;
forcedConfig[key] = [self expressionForPattern:pattern];
}
}
#endif
[self applyOverrides:forcedConfig];
return forcedConfig;
}

View File

@@ -0,0 +1,102 @@
/// Copyright 2024 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
@interface SNTConfigurator (Testing)
- (instancetype)initWithSyncStateFile:(NSString *)syncStateFilePath
syncStateAccessAuthorizer:(BOOL (^)(void))syncStateAccessAuthorizer;
@property NSDictionary *syncState;
@end
@interface SNTConfiguratorTest : XCTestCase
@property NSFileManager *fileMgr;
@property NSString *testDir;
@end
@implementation SNTConfiguratorTest
- (void)setUp {
self.fileMgr = [NSFileManager defaultManager];
self.testDir =
[NSString stringWithFormat:@"%@santa-configurator-%d", NSTemporaryDirectory(), getpid()];
XCTAssertTrue([self.fileMgr createDirectoryAtPath:self.testDir
withIntermediateDirectories:YES
attributes:nil
error:nil]);
}
- (void)tearDown {
XCTAssertTrue([self.fileMgr removeItemAtPath:self.testDir error:nil]);
}
- (void)runMigrationTestsWithSyncState:(NSDictionary *)syncStatePlist
verifier:(void (^)(SNTConfigurator *))verifierBlock {
NSString *syncStatePlistPath =
[NSString stringWithFormat:@"%@/test-sync-state.plist", self.testDir];
XCTAssertTrue([syncStatePlist writeToFile:syncStatePlistPath atomically:YES]);
SNTConfigurator *cfg = [[SNTConfigurator alloc] initWithSyncStateFile:syncStatePlistPath
syncStateAccessAuthorizer:^{
// Allow all access to the test plist
return YES;
}];
NSLog(@"sync state: %@", cfg.syncState);
verifierBlock(cfg);
XCTAssertTrue([self.fileMgr removeItemAtPath:syncStatePlistPath error:nil]);
}
- (void)testInitMigratesSyncStateKeys {
// SyncCleanRequired = YES
[self runMigrationTestsWithSyncState:@{@"SyncCleanRequired" : [NSNumber numberWithBool:YES]}
verifier:^(SNTConfigurator *cfg) {
XCTAssertEqual(cfg.syncState.count, 1);
XCTAssertNil(cfg.syncState[@"SyncCleanRequired"]);
XCTAssertNotNil(cfg.syncState[@"SyncTypeRequired"]);
XCTAssertEqual([cfg.syncState[@"SyncTypeRequired"] integerValue],
SNTSyncTypeClean);
XCTAssertEqual(cfg.syncState.count, 1);
}];
// SyncCleanRequired = NO
[self runMigrationTestsWithSyncState:@{@"SyncCleanRequired" : [NSNumber numberWithBool:NO]}
verifier:^(SNTConfigurator *cfg) {
XCTAssertEqual(cfg.syncState.count, 1);
XCTAssertNil(cfg.syncState[@"SyncCleanRequired"]);
XCTAssertNotNil(cfg.syncState[@"SyncTypeRequired"]);
XCTAssertEqual([cfg.syncState[@"SyncTypeRequired"] integerValue],
SNTSyncTypeNormal);
XCTAssertEqual(cfg.syncState.count, 1);
}];
// Empty state
[self runMigrationTestsWithSyncState:@{}
verifier:^(SNTConfigurator *cfg) {
XCTAssertEqual(cfg.syncState.count, 0);
XCTAssertNil(cfg.syncState[@"SyncCleanRequired"]);
XCTAssertNil(cfg.syncState[@"SyncTypeRequired"]);
}];
}
@end

View File

@@ -0,0 +1,27 @@
/// Copyright 2023 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <Foundation/Foundation.h>
@interface NSArray (SNTDeepCopy)
- (instancetype)sntDeepCopy;
@end
@interface NSDictionary (SNTDeepCopy)
- (instancetype)sntDeepCopy;
@end

View File

@@ -0,0 +1,53 @@
/// Copyright 2023 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/common/SNTDeepCopy.h"
@implementation NSArray (SNTDeepCopy)
- (instancetype)sntDeepCopy {
NSMutableArray<__kindof NSObject *> *deepCopy = [NSMutableArray arrayWithCapacity:self.count];
for (id object in self) {
if ([object respondsToSelector:@selector(sntDeepCopy)]) {
[deepCopy addObject:[object sntDeepCopy]];
} else if ([object respondsToSelector:@selector(copyWithZone:)]) {
[deepCopy addObject:[object copy]];
} else {
[deepCopy addObject:object];
}
}
return deepCopy;
}
@end
@implementation NSDictionary (SNTDeepCopy)
- (instancetype)sntDeepCopy {
NSMutableDictionary<__kindof NSObject *, __kindof NSObject *> *deepCopy =
[NSMutableDictionary dictionary];
for (id key in self) {
id value = self[key];
if ([value respondsToSelector:@selector(sntDeepCopy)]) {
deepCopy[key] = [value sntDeepCopy];
} else if ([value respondsToSelector:@selector(copyWithZone:)]) {
deepCopy[key] = [value copy];
} else {
deepCopy[key] = value;
}
}
return deepCopy;
}
@end

View File

@@ -14,12 +14,12 @@
#import <Foundation/Foundation.h>
#import <MOLCertificate/MOLCertificate.h>
#import "Source/common/SNTStoredEvent.h"
///
/// Represents an event stored in the database.
///
@interface SNTFileAccessEvent : NSObject <NSSecureCoding>
@interface SNTFileAccessEvent : SNTStoredEvent <NSSecureCoding>
///
/// The watched path that was accessed
@@ -32,57 +32,11 @@
@property NSString *ruleVersion;
@property NSString *ruleName;
///
/// The SHA256 of the process that accessed the path
///
@property NSString *fileSHA256;
///
/// The path of the process that accessed the watched path
///
@property NSString *filePath;
///
/// If the process is part of a bundle, the name of the application
///
@property NSString *application;
///
/// If the executed file was signed, this is the Team ID if present in the signature information.
///
@property NSString *teamID;
///
/// If the executed file was signed, this is the Signing ID if present in the signature information.
///
@property NSString *signingID;
///
/// The user who executed the binary.
///
@property NSString *executingUser;
///
/// The process ID of the binary being executed.
///
@property NSNumber *pid;
///
/// The parent process ID of the binary being executed.
///
@property NSNumber *ppid;
///
/// The name of the parent process.
///
@property NSString *parentName;
///
/// If the executed file was signed, this is an NSArray of MOLCertificate's
/// representing the signing chain.
///
@property NSArray<MOLCertificate *> *signingChain;
///
/// A string representing the publisher based on the signingChain
///

View File

@@ -48,35 +48,20 @@
}
- (void)encodeWithCoder:(NSCoder *)coder {
[super encodeWithCoder:coder];
ENCODE(accessedPath);
ENCODE(ruleVersion);
ENCODE(ruleName);
ENCODE(fileSHA256);
ENCODE(filePath);
ENCODE(application);
ENCODE(teamID);
ENCODE(teamID);
ENCODE(pid);
ENCODE(ppid);
ENCODE(parentName);
ENCODE(signingChain);
}
- (instancetype)initWithCoder:(NSCoder *)decoder {
self = [super init];
self = [super initWithCoder:decoder];
if (self) {
DECODE(accessedPath, NSString);
DECODE(ruleVersion, NSString);
DECODE(ruleName, NSString);
DECODE(fileSHA256, NSString);
DECODE(filePath, NSString);
DECODE(application, NSString);
DECODE(teamID, NSString);
DECODE(teamID, NSString);
DECODE(pid, NSNumber);
DECODE(ppid, NSNumber);
DECODE(parentName, NSString);
DECODEARRAY(signingChain, MOLCertificate);
}
return self;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
/// Copyright 2024 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
/**
* This file declares two types that are mirrors of each other.
*
* The C struct serves as a way to group and pass valid rule identifiers around
* in order to minimize interface changes needed when new rule types are added
* and also alleviate the need to allocate a short lived object.
*
* The Objective C class is used for an XPC boundary to easily pass rule
* identifiers between Santa components.
*/
#import <Foundation/Foundation.h>
struct RuleIdentifiers {
NSString *cdhash;
NSString *binarySHA256;
NSString *signingID;
NSString *certificateSHA256;
NSString *teamID;
};
@interface SNTRuleIdentifiers : NSObject <NSSecureCoding>
@property(readonly) NSString *cdhash;
@property(readonly) NSString *binarySHA256;
@property(readonly) NSString *signingID;
@property(readonly) NSString *certificateSHA256;
@property(readonly) NSString *teamID;
/// Please use `initWithRuleIdentifiers:`
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithRuleIdentifiers:(struct RuleIdentifiers)identifiers
NS_DESIGNATED_INITIALIZER;
- (struct RuleIdentifiers)toStruct;
@end

View File

@@ -0,0 +1,73 @@
/// Copyright 2024 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/common/SNTRuleIdentifiers.h"
@implementation SNTRuleIdentifiers
- (instancetype)initWithRuleIdentifiers:(struct RuleIdentifiers)identifiers {
self = [super init];
if (self) {
_cdhash = identifiers.cdhash;
_binarySHA256 = identifiers.binarySHA256;
_signingID = identifiers.signingID;
_certificateSHA256 = identifiers.certificateSHA256;
_teamID = identifiers.teamID;
}
return self;
}
- (struct RuleIdentifiers)toStruct {
return (struct RuleIdentifiers){.cdhash = self.cdhash,
.binarySHA256 = self.binarySHA256,
.signingID = self.signingID,
.certificateSHA256 = self.certificateSHA256,
.teamID = self.teamID};
}
#pragma mark NSSecureCoding
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-literal-conversion"
#define ENCODE(obj, key) \
if (obj) [coder encodeObject:obj forKey:key]
#define DECODE(cls, key) [decoder decodeObjectOfClass:[cls class] forKey:key]
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)initWithCoder:(NSCoder *)decoder {
self = [self init];
if (self) {
_cdhash = DECODE(NSString, @"cdhash");
_binarySHA256 = DECODE(NSString, @"binarySHA256");
_signingID = DECODE(NSString, @"signingID");
_certificateSHA256 = DECODE(NSString, @"certificateSHA256");
_teamID = DECODE(NSString, @"teamID");
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
ENCODE(self.cdhash, @"cdhash");
ENCODE(self.binarySHA256, @"binarySHA256");
ENCODE(self.signingID, @"signingID");
ENCODE(self.certificateSHA256, @"certificateSHA256");
ENCODE(self.teamID, @"teamID");
}
#pragma clang diagnostic pop
@end

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,8 @@ extern NSString *const kClientModeMonitor;
extern NSString *const kClientModeLockdown;
extern NSString *const kBlockUSBMount;
extern NSString *const kRemountUSBMode;
extern NSString *const kCleanSync;
extern NSString *const kCleanSyncDeprecated;
extern NSString *const kSyncType;
extern NSString *const kAllowedPathRegex;
extern NSString *const kAllowedPathRegexDeprecated;
extern NSString *const kBlockedPathRegex;
@@ -43,6 +44,7 @@ extern NSString *const kCompilerRuleCount;
extern NSString *const kTransitiveRuleCount;
extern NSString *const kTeamIDRuleCount;
extern NSString *const kSigningIDRuleCount;
extern NSString *const kCDHashRuleCount;
extern NSString *const kFullSyncInterval;
extern NSString *const kFCMToken;
extern NSString *const kFCMFullSyncInterval;
@@ -69,12 +71,14 @@ extern NSString *const kDecisionAllowCertificate;
extern NSString *const kDecisionAllowScope;
extern NSString *const kDecisionAllowTeamID;
extern NSString *const kDecisionAllowSigningID;
extern NSString *const kDecisionAllowCDHash;
extern NSString *const kDecisionBlockUnknown;
extern NSString *const kDecisionBlockBinary;
extern NSString *const kDecisionBlockCertificate;
extern NSString *const kDecisionBlockScope;
extern NSString *const kDecisionBlockTeamID;
extern NSString *const kDecisionBlockSigningID;
extern NSString *const kDecisionBlockCDHash;
extern NSString *const kDecisionUnknown;
extern NSString *const kDecisionBundleBinary;
extern NSString *const kLoggedInUsers;
@@ -100,6 +104,7 @@ extern NSString *const kCertValidFrom;
extern NSString *const kCertValidUntil;
extern NSString *const kTeamID;
extern NSString *const kSigningID;
extern NSString *const kCDHash;
extern NSString *const kQuarantineDataURL;
extern NSString *const kQuarantineRefererURL;
extern NSString *const kQuarantineTimestamp;
@@ -124,6 +129,7 @@ extern NSString *const kRuleTypeBinary;
extern NSString *const kRuleTypeCertificate;
extern NSString *const kRuleTypeTeamID;
extern NSString *const kRuleTypeSigningID;
extern NSString *const kRuleTypeCDHash;
extern NSString *const kRuleCustomMsg;
extern NSString *const kRuleCustomURL;
extern NSString *const kCursor;

View File

@@ -32,7 +32,8 @@ NSString *const kBlockUSBMount = @"block_usb_mount";
NSString *const kRemountUSBMode = @"remount_usb_mode";
NSString *const kClientModeMonitor = @"MONITOR";
NSString *const kClientModeLockdown = @"LOCKDOWN";
NSString *const kCleanSync = @"clean_sync";
NSString *const kCleanSyncDeprecated = @"clean_sync";
NSString *const kSyncType = @"sync_type";
NSString *const kAllowedPathRegex = @"allowed_path_regex";
NSString *const kAllowedPathRegexDeprecated = @"whitelist_regex";
NSString *const kBlockedPathRegex = @"blocked_path_regex";
@@ -43,6 +44,7 @@ NSString *const kCompilerRuleCount = @"compiler_rule_count";
NSString *const kTransitiveRuleCount = @"transitive_rule_count";
NSString *const kTeamIDRuleCount = @"teamid_rule_count";
NSString *const kSigningIDRuleCount = @"signingid_rule_count";
NSString *const kCDHashRuleCount = @"cdhash_rule_count";
NSString *const kFullSyncInterval = @"full_sync_interval";
NSString *const kFCMToken = @"fcm_token";
NSString *const kFCMFullSyncInterval = @"fcm_full_sync_interval";
@@ -70,12 +72,14 @@ NSString *const kDecisionAllowCertificate = @"ALLOW_CERTIFICATE";
NSString *const kDecisionAllowScope = @"ALLOW_SCOPE";
NSString *const kDecisionAllowTeamID = @"ALLOW_TEAMID";
NSString *const kDecisionAllowSigningID = @"ALLOW_SIGNINGID";
NSString *const kDecisionAllowCDHash = @"ALLOW_CDHASH";
NSString *const kDecisionBlockUnknown = @"BLOCK_UNKNOWN";
NSString *const kDecisionBlockBinary = @"BLOCK_BINARY";
NSString *const kDecisionBlockCertificate = @"BLOCK_CERTIFICATE";
NSString *const kDecisionBlockScope = @"BLOCK_SCOPE";
NSString *const kDecisionBlockTeamID = @"BLOCK_TEAMID";
NSString *const kDecisionBlockSigningID = @"BLOCK_SIGNINGID";
NSString *const kDecisionBlockCDHash = @"BLOCK_CDHASH";
NSString *const kDecisionUnknown = @"UNKNOWN";
NSString *const kDecisionBundleBinary = @"BUNDLE_BINARY";
NSString *const kLoggedInUsers = @"logged_in_users";
@@ -101,6 +105,7 @@ NSString *const kCertValidFrom = @"valid_from";
NSString *const kCertValidUntil = @"valid_until";
NSString *const kTeamID = @"team_id";
NSString *const kSigningID = @"signing_id";
NSString *const kCDHash = @"cdhash";
NSString *const kQuarantineDataURL = @"quarantine_data_url";
NSString *const kQuarantineRefererURL = @"quarantine_referer_url";
NSString *const kQuarantineTimestamp = @"quarantine_timestamp";
@@ -125,6 +130,7 @@ NSString *const kRuleTypeBinary = @"BINARY";
NSString *const kRuleTypeCertificate = @"CERTIFICATE";
NSString *const kRuleTypeTeamID = @"TEAMID";
NSString *const kRuleTypeSigningID = @"SIGNINGID";
NSString *const kRuleTypeCDHash = @"CDHASH";
NSString *const kRuleCustomMsg = @"custom_msg";
NSString *const kRuleCustomURL = @"custom_url";
NSString *const kCursor = @"cursor";

View File

@@ -12,6 +12,7 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/common/SNTRuleIdentifiers.h"
#import "Source/common/SNTXPCUnprivilegedControlInterface.h"
///
@@ -28,15 +29,12 @@
/// Database ops
///
- (void)databaseRuleAddRules:(NSArray *)rules
cleanSlate:(BOOL)cleanSlate
ruleCleanup:(SNTRuleCleanup)cleanupType
reply:(void (^)(NSError *error))reply;
- (void)databaseEventsPending:(void (^)(NSArray *events))reply;
- (void)databaseRemoveEventsWithIDs:(NSArray *)ids;
- (void)databaseRuleForBinarySHA256:(NSString *)binarySHA256
certificateSHA256:(NSString *)certificateSHA256
teamID:(NSString *)teamID
signingID:(NSString *)signingID
reply:(void (^)(SNTRule *))reply;
- (void)databaseRuleForIdentifiers:(SNTRuleIdentifiers *)identifiers
reply:(void (^)(SNTRule *))reply;
- (void)retrieveAllRules:(void (^)(NSArray<SNTRule *> *rules, NSError *error))reply;
///
@@ -45,7 +43,7 @@
- (void)setClientMode:(SNTClientMode)mode reply:(void (^)(void))reply;
- (void)setFullSyncLastSuccess:(NSDate *)date reply:(void (^)(void))reply;
- (void)setRuleSyncLastSuccess:(NSDate *)date reply:(void (^)(void))reply;
- (void)setSyncCleanRequired:(BOOL)cleanReqd reply:(void (^)(void))reply;
- (void)setSyncTypeRequired:(SNTSyncType)syncType reply:(void (^)(void))reply;
- (void)setAllowedPathRegex:(NSString *)pattern reply:(void (^)(void))reply;
- (void)setBlockedPathRegex:(NSString *)pattern reply:(void (^)(void))reply;
- (void)setBlockUSBMount:(BOOL)enabled reply:(void (^)(void))reply;

View File

@@ -34,8 +34,7 @@ NSString *const kBundleID = @"com.google.santa.daemon";
#else
MOLCodesignChecker *cs = [[MOLCodesignChecker alloc] initWithSelf];
// "teamid.com.google.santa.daemon.xpc"
NSString *t = cs.signingInformation[@"teamid"];
return [NSString stringWithFormat:@"%@.%@.xpc", t, kBundleID];
return [NSString stringWithFormat:@"%@.%@.xpc", cs.teamID, kBundleID];
#endif
}
@@ -50,7 +49,7 @@ NSString *const kBundleID = @"com.google.santa.daemon";
ofReply:YES];
[r setClasses:[NSSet setWithObjects:[NSArray class], [SNTRule class], nil]
forSelector:@selector(databaseRuleAddRules:cleanSlate:reply:)
forSelector:@selector(databaseRuleAddRules:ruleCleanup:reply:)
argumentIndex:0
ofReply:NO];

View File

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

View File

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

View File

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

View File

@@ -245,7 +245,7 @@ struct S {
uint64_t first_val;
uint64_t second_val;
bool operator==(const S &rhs) {
bool operator==(const S &rhs) const {
return first_val == rhs.first_val && second_val == rhs.second_val;
}
};

View File

@@ -0,0 +1,29 @@
/// Copyright 2023 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#ifndef SANTA__COMMON__SCOPEDCFTYPEREF_H
#define SANTA__COMMON__SCOPEDCFTYPEREF_H
#include <CoreFoundation/CoreFoundation.h>
#include "Source/common/ScopedTypeRef.h"
namespace santa::common {
template <typename CFT>
using ScopedCFTypeRef = ScopedTypeRef<CFT, (CFT)NULL, CFRetain, CFRelease>;
} // namespace santa::common
#endif

View File

@@ -0,0 +1,141 @@
/// Copyright 2023 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>
#import <XCTest/XCTest.h>
#include "XCTest/XCTest.h"
#include "Source/common/ScopedCFTypeRef.h"
using santa::common::ScopedCFTypeRef;
@interface ScopedCFTypeRefTest : XCTestCase
@end
@implementation ScopedCFTypeRefTest
- (void)testDefaultConstruction {
// Default construction creates wraps a NULL object
ScopedCFTypeRef<CFNumberRef> scopedRef;
XCTAssertFalse(scopedRef.Unsafe());
}
- (void)testOperatorBool {
// Operator bool is `false` when object is null
{
ScopedCFTypeRef<CFNumberRef> scopedNullRef;
XCTAssertFalse(scopedNullRef.Unsafe());
XCTAssertFalse(scopedNullRef);
}
// Operator bool is `true` when object is NOT null
{
int x = 123;
CFNumberRef numRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &x);
ScopedCFTypeRef<CFNumberRef> scopedNumRef = ScopedCFTypeRef<CFNumberRef>::Assume(numRef);
XCTAssertTrue(scopedNumRef.Unsafe());
XCTAssertTrue(scopedNumRef);
}
}
// Note that CFMutableArray is used for testing, even when subtypes aren't
// needed, because it is never optimized into immortal constant values, unlike
// other types.
- (void)testAssume {
int want = 123;
int got = 0;
CFMutableArrayRef array = CFArrayCreateMutable(nullptr, /*capacity=*/0, &kCFTypeArrayCallBacks);
// Baseline state, initial retain count is 1 after object creation
XCTAssertEqual(1, CFGetRetainCount(array));
CFNumberRef numRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &want);
CFArrayAppendValue(array, numRef);
CFRelease(numRef);
XCTAssertEqual(1, CFArrayGetCount(array));
{
ScopedCFTypeRef<CFMutableArrayRef> scopedArray =
ScopedCFTypeRef<CFMutableArrayRef>::Assume(array);
// Ensure ownership was taken, and retain count remains unchanged
XCTAssertTrue(scopedArray.Unsafe());
XCTAssertEqual(1, CFGetRetainCount(scopedArray.Unsafe()));
// Make sure the object contains expected contents
CFMutableArrayRef ref = scopedArray.Unsafe();
XCTAssertEqual(1, CFArrayGetCount(ref));
XCTAssertTrue(
CFNumberGetValue((CFNumberRef)CFArrayGetValueAtIndex(ref, 0), kCFNumberIntType, &got));
XCTAssertEqual(want, got);
}
}
// Note that CFMutableArray is used for testing, even when subtypes aren't
// needed, because it is never optimized into immortal constant values, unlike
// other types.
- (void)testRetain {
int want = 123;
int got = 0;
CFMutableArrayRef array = CFArrayCreateMutable(nullptr, /*capacity=*/0, &kCFTypeArrayCallBacks);
// Baseline state, initial retain count is 1 after object creation
XCTAssertEqual(1, CFGetRetainCount(array));
CFNumberRef numRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &want);
CFArrayAppendValue(array, numRef);
CFRelease(numRef);
XCTAssertEqual(1, CFArrayGetCount(array));
{
ScopedCFTypeRef<CFMutableArrayRef> scopedArray =
ScopedCFTypeRef<CFMutableArrayRef>::Retain(array);
// Ensure ownership was taken, and retain count was incremented
XCTAssertTrue(scopedArray.Unsafe());
XCTAssertEqual(2, CFGetRetainCount(scopedArray.Unsafe()));
// Make sure the object contains expected contents
CFMutableArrayRef ref = scopedArray.Unsafe();
XCTAssertEqual(1, CFArrayGetCount(ref));
XCTAssertTrue(
CFNumberGetValue((CFNumberRef)CFArrayGetValueAtIndex(ref, 0), kCFNumberIntType, &got));
XCTAssertEqual(want, got);
}
// The original `array` object should still be valid due to the extra retain.
// Ensure the retain count has decreased since `scopedArray` went out of scope
XCTAssertEqual(1, CFArrayGetCount(array));
}
- (void)testInto {
ScopedCFTypeRef<CFURLRef> scopedURLRef =
ScopedCFTypeRef<CFURLRef>::Assume(CFURLCreateWithFileSystemPath(
kCFAllocatorDefault, CFSTR("/usr/bin/true"), kCFURLPOSIXPathStyle, YES));
ScopedCFTypeRef<SecStaticCodeRef> scopedCodeRef;
XCTAssertFalse(scopedCodeRef);
SecStaticCodeCreateWithPath(scopedURLRef.Unsafe(), kSecCSDefaultFlags,
scopedCodeRef.InitializeInto());
// Ensure the scoped object was initialized
XCTAssertTrue(scopedCodeRef);
}
@end

View File

@@ -0,0 +1,30 @@
/// Copyright 2023 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#ifndef SANTA__COMMON__SCOPEDIOOBJECTREF_H
#define SANTA__COMMON__SCOPEDIOOBJECTREF_H
#include <IOKit/IOKitLib.h>
#include "Source/common/ScopedTypeRef.h"
namespace santa::common {
template <typename IOT>
using ScopedIOObjectRef =
ScopedTypeRef<IOT, (IOT)IO_OBJECT_NULL, IOObjectRetain, IOObjectRelease>;
}
#endif

View File

@@ -0,0 +1,104 @@
/// Copyright 2023 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/usb/IOUSBLib.h>
#import <XCTest/XCTest.h>
#include "Source/common/ScopedIOObjectRef.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
using santa::common::ScopedIOObjectRef;
using santa::santad::logs::endpoint_security::serializers::Utilities::GetDefaultIOKitCommsPort;
@interface ScopedIOObjectRefTest : XCTestCase
@end
@implementation ScopedIOObjectRefTest
- (void)testDefaultConstruction {
// Default construction creates wraps a NULL object
ScopedIOObjectRef<io_object_t> scopedRef;
XCTAssertFalse(scopedRef.Unsafe());
}
- (void)testOperatorBool {
// Operator bool is `false` when object is null
{
ScopedIOObjectRef<io_object_t> scopedNullRef;
XCTAssertFalse(scopedNullRef.Unsafe());
XCTAssertFalse(scopedNullRef);
}
// Operator bool is `true` when object is NOT null
{
CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOUSBDeviceClassName);
XCTAssertNotEqual((CFMutableDictionaryRef)NULL, matchingDict);
io_service_t service = IOServiceGetMatchingService(GetDefaultIOKitCommsPort(), matchingDict);
ScopedIOObjectRef<io_service_t> scopedServiceRef =
ScopedIOObjectRef<io_service_t>::Assume(service);
XCTAssertTrue(scopedServiceRef.Unsafe());
XCTAssertTrue(scopedServiceRef);
}
}
- (void)testAssume {
CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOUSBDeviceClassName);
XCTAssertNotEqual((CFMutableDictionaryRef)NULL, matchingDict);
io_service_t service = IOServiceGetMatchingService(GetDefaultIOKitCommsPort(), matchingDict);
// Baseline state, initial retain count is 1 after object creation
XCTAssertEqual(1, IOObjectGetUserRetainCount(service));
XCTAssertNotEqual(IO_OBJECT_NULL, service);
{
ScopedIOObjectRef<io_service_t> scopedIORef = ScopedIOObjectRef<io_service_t>::Assume(service);
// Ensure ownership was taken, and retain count remains unchanged
XCTAssertTrue(scopedIORef.Unsafe());
XCTAssertEqual(1, IOObjectGetUserRetainCount(scopedIORef.Unsafe()));
XCTAssertNotEqual(IO_OBJECT_NULL, scopedIORef.Unsafe());
}
}
- (void)testRetain {
CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOUSBDeviceClassName);
XCTAssertNotEqual((CFMutableDictionaryRef)NULL, matchingDict);
io_service_t service = IOServiceGetMatchingService(GetDefaultIOKitCommsPort(), matchingDict);
// Baseline state, initial retain count is 1 after object creation
XCTAssertEqual(1, IOObjectGetUserRetainCount(service));
XCTAssertNotEqual(IO_OBJECT_NULL, service);
{
ScopedIOObjectRef<io_service_t> scopedIORef = ScopedIOObjectRef<io_service_t>::Retain(service);
// Ensure ownership was taken, and retain count was incremented
XCTAssertTrue(scopedIORef.Unsafe());
XCTAssertEqual(2, IOObjectGetUserRetainCount(scopedIORef.Unsafe()));
XCTAssertNotEqual(IO_OBJECT_NULL, scopedIORef.Unsafe());
}
// The original `service` object should still be valid due to the extra retain.
// Ensure the retain count has decreased since `scopedIORef` went out of scope.
XCTAssertEqual(1, IOObjectGetUserRetainCount(service));
}
@end

View File

@@ -0,0 +1,80 @@
/// Copyright 2023 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#ifndef SANTA__COMMON__SCOPEDTYPEREF_H
#define SANTA__COMMON__SCOPEDTYPEREF_H
#include <CoreFoundation/CoreFoundation.h>
#include <assert.h>
namespace santa::common {
template <typename ElementT, ElementT InvalidV, auto RetainFunc,
auto ReleaseFunc>
class ScopedTypeRef {
public:
ScopedTypeRef() : object_(InvalidV) {}
// Can be implemented safely, but not currently needed
ScopedTypeRef(ScopedTypeRef&& other) = delete;
ScopedTypeRef& operator=(ScopedTypeRef&& rhs) = delete;
ScopedTypeRef(const ScopedTypeRef& other) = delete;
ScopedTypeRef& operator=(const ScopedTypeRef& other) = delete;
// Take ownership of a given object
static ScopedTypeRef<ElementT, InvalidV, RetainFunc, ReleaseFunc> Assume(
ElementT object) {
return ScopedTypeRef<ElementT, InvalidV, RetainFunc, ReleaseFunc>(object);
}
// Retain and take ownership of a given object
static ScopedTypeRef<ElementT, InvalidV, RetainFunc, ReleaseFunc> Retain(
ElementT object) {
if (object) {
RetainFunc(object);
}
return ScopedTypeRef<ElementT, InvalidV, RetainFunc, ReleaseFunc>(object);
}
~ScopedTypeRef() {
if (object_) {
ReleaseFunc(object_);
object_ = InvalidV;
}
}
explicit operator bool() { return object_ != InvalidV; }
ElementT Unsafe() { return object_; }
// This is to be used only to take ownership of objects that are created by
// pass-by-pointer create functions. The object must not already be valid.
// In non-opt builds, this is enforced by an assert that will terminate the
// process.
ElementT* InitializeInto() {
assert(object_ == InvalidV);
return &object_;
}
private:
// Not API.
// Use Assume or Retain static methods.
ScopedTypeRef(ElementT object) : object_(object) {}
ElementT object_;
};
} // namespace santa::common
#endif

View File

@@ -92,7 +92,11 @@ es_process_t MakeESProcess(es_file_t *file, audit_token_t tok, audit_token_t par
}
uint32_t MaxSupportedESMessageVersionForCurrentOS() {
// Note: ES message v3 was only in betas.
// Notes:
// 1. ES message v3 was only in betas.
// 2. Message version 7 appeared in macOS 13.3, but features from that are
// not currently used. Leaving off support here so as to not require
// adding v7 test JSON files.
if (@available(macOS 13.0, *)) {
return 6;
} else if (@available(macOS 12.3, *)) {

View File

@@ -4,6 +4,7 @@ syntax = "proto3";
import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";
import "Source/santad/ProcessTree/process_tree.proto";
option objc_class_prefix = "SNTPB";
@@ -173,6 +174,8 @@ message ProcessInfo {
// Time the process was started
optional google.protobuf.Timestamp start_time = 17;
optional process_tree.Annotations annotations = 18;
}
// Light variant of ProcessInfo message to help minimize on-disk/on-wire sizes
@@ -202,6 +205,8 @@ message ProcessInfoLight {
// File information for the executable backing this process
optional FileInfoLight executable = 10;
optional process_tree.Annotations annotations = 11;
}
// Certificate information
@@ -213,6 +218,27 @@ message CertificateInfo {
optional string common_name = 2;
}
// Information about a single entitlement key/value pair
message Entitlement {
// The name of an entitlement
optional string key = 1;
// The value of an entitlement
optional string value = 2;
}
// Information about entitlements
message EntitlementInfo {
// Whether or not the set of reported entilements is complete or has been
// filtered (e.g. by configuration or clipped because too many to log).
optional bool entitlements_filtered = 1;
// The set of entitlements associated with the target executable
// Only top level keys are represented
// Values (including nested keys) are JSON serialized
repeated Entitlement entitlements = 2;
}
// Information about a process execution event
message Execution {
// The process that executed the new image (e.g. the process that called
@@ -263,6 +289,7 @@ message Execution {
REASON_LONG_PATH = 9;
REASON_NOT_RUNNING = 10;
REASON_SIGNING_ID = 11;
REASON_CDHASH = 12;
}
optional Reason reason = 10;
@@ -286,6 +313,9 @@ message Execution {
// The original path on disk of the target executable
// Applies when executables are translocated
optional string original_path = 15;
// Entitlement information about the target executbale
optional EntitlementInfo entitlement_info = 16;
}
// Information about a fork event

View File

@@ -1,7 +1,7 @@
<?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">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22689"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@@ -18,18 +18,18 @@
</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"/>
<window title="Santa Blocked Execution" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" visibleAtLaunch="NO" animationBehavior="none" titlebarAppearsTransparent="YES" titleVisibility="hidden" id="9Bq-yh-54f" customClass="SNTMessageWindow">
<windowStyleMask key="styleMask" titled="YES" utility="YES"/>
<rect key="contentRect" x="167" y="107" width="540" height="479"/>
<rect key="screenRect" x="0.0" y="0.0" width="1728" height="1079"/>
<rect key="screenRect" x="0.0" y="0.0" width="1916" height="1099"/>
<view key="contentView" id="Iwq-Lx-rLv">
<rect key="frame" x="0.0" y="0.0" width="540" height="462"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kiB-jZ-69S">
<rect key="frame" x="16" y="434" width="37" height="32"/>
<rect key="frame" x="16" y="434" width="37" height="23"/>
<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">
<buttonCell key="cell" type="roundTextured" title="Hidden Button" alternateTitle="This button exists so neither of the other two buttons is pre-selected when the dialog opens." bezelStyle="texturedRounded" 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>
@@ -128,10 +128,6 @@
</textField>
<button toolTip="Show code signing certificate chain" translatesAutoresizingMaskIntoConstraints="NO" id="cJf-k6-OxS" userLabel="Publisher Certs Button">
<rect key="frame" x="62" y="235" width="15" height="15"/>
<constraints>
<constraint firstAttribute="width" constant="15" id="QTm-Iv-m5p"/>
<constraint firstAttribute="height" constant="15" id="YwG-0s-jop"/>
</constraints>
<buttonCell key="cell" type="bevel" bezelStyle="regularSquare" image="NSInfo" imagePosition="overlaps" alignment="center" refusesFirstResponder="YES" imageScaling="proportionallyDown" inset="2" id="R72-Qy-Xbb">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@@ -139,6 +135,10 @@
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
</userDefinedRuntimeAttributes>
</buttonCell>
<constraints>
<constraint firstAttribute="width" constant="15" id="QTm-Iv-m5p"/>
<constraint firstAttribute="height" constant="15" id="YwG-0s-jop"/>
</constraints>
<connections>
<action selector="showCertInfo:" target="-2" id="dB0-a3-X31"/>
<binding destination="-2" name="hidden" keyPath="self.publisherInfo" id="fFR-f3-Oiw">
@@ -241,16 +241,17 @@
</textField>
<button verticalHuggingPriority="750" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="7ua-5a-uSd">
<rect key="frame" x="147" y="30" width="126" height="32"/>
<constraints>
<constraint firstAttribute="width" priority="900" constant="112" id="Pec-Pa-4aZ"/>
</constraints>
<buttonCell key="cell" type="push" title="Open Event..." bezelStyle="rounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="X1b-TF-1TL">
<buttonCell key="cell" type="push" title="Open..." bezelStyle="rounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="X1b-TF-1TL">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
DQ
</string>
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</buttonCell>
<constraints>
<constraint firstAttribute="width" priority="900" constant="112" id="Pec-Pa-4aZ"/>
</constraints>
<connections>
<action selector="openEventDetails:" target="-2" id="VhL-ql-rCV"/>
<outlet property="nextKeyView" destination="BbV-3h-mmL" id="Xkz-va-iGc"/>
@@ -272,23 +273,19 @@ DQ
</textField>
<button translatesAutoresizingMaskIntoConstraints="NO" id="5D8-GP-a4l">
<rect key="frame" x="110" y="80" width="319" height="29"/>
<constraints>
<constraint firstAttribute="height" constant="25" id="KvD-X6-CsO"/>
</constraints>
<buttonCell key="cell" type="check" title="Prevent future notifications for this application for a day" bezelStyle="regularSquare" imagePosition="left" alignment="center" inset="2" id="R5Y-Uc-rEP">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="smallSystem"/>
</buttonCell>
<constraints>
<constraint firstAttribute="height" constant="25" id="KvD-X6-CsO"/>
</constraints>
<connections>
<binding destination="-2" name="value" keyPath="self.silenceFutureNotifications" id="tEb-2A-sht"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="BbV-3h-mmL" userLabel="Dismiss Button">
<rect key="frame" x="271" y="28" width="124" height="34"/>
<constraints>
<constraint firstAttribute="width" constant="110" id="6Uh-Bd-N64"/>
<constraint firstAttribute="height" constant="22" id="GH6-nw-6rD"/>
</constraints>
<buttonCell key="cell" type="push" title="Ignore" bezelStyle="rounded" 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"/>
@@ -296,6 +293,10 @@ DQ
Gw
</string>
</buttonCell>
<constraints>
<constraint firstAttribute="width" constant="110" id="6Uh-Bd-N64"/>
<constraint firstAttribute="height" constant="22" id="GH6-nw-6rD"/>
</constraints>
<accessibility description="Dismiss Dialog"/>
<connections>
<action selector="closeWindow:" target="-2" id="qQq-gh-8lw"/>

View File

@@ -92,6 +92,10 @@
NSString *eventDetailText = [[SNTConfigurator configurator] eventDetailText];
if (eventDetailText) {
[self.openEventButton setTitle:eventDetailText];
// Require the button keyEquivalent set to be CMD + Return
[self.openEventButton setKeyEquivalent:@"\r"]; // Return Key
[self.openEventButton
setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; // Command Key
}
}

View File

@@ -142,6 +142,8 @@ struct SNTFileAccessMessageWindowView: View {
Text(customText ?? "Open Event...").frame(maxWidth:.infinity)
})
.buttonStyle(.borderedProminent)
.keyboardShortcut(.defaultAction)
}
Button(action: dismissButton, label: {
Text("Dismiss").frame(maxWidth:.infinity)

View File

@@ -226,6 +226,15 @@
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
se.signingChain = cs.certificates;
se.cdhash = cs.cdhash;
se.teamID = cs.teamID;
if (cs.signingID) {
if (cs.teamID) {
se.signingID = [NSString stringWithFormat:@"%@:%@", cs.teamID, cs.signingID];
} else if (cs.platformBinary) {
se.signingID = [NSString stringWithFormat:@"platform:%@", cs.signingID];
}
}
dispatch_sync(dispatch_get_main_queue(), ^{
relatedEvents[se.fileSHA256] = se;

View File

@@ -41,6 +41,7 @@ objc_library(
"Commands/SNTCommandFileInfo.m",
"Commands/SNTCommandMetrics.h",
"Commands/SNTCommandMetrics.m",
"Commands/SNTCommandRule.h",
"Commands/SNTCommandRule.m",
"Commands/SNTCommandStatus.m",
"Commands/SNTCommandSync.m",
@@ -67,6 +68,7 @@ objc_library(
"//Source/common:SNTLogging",
"//Source/common:SNTMetricSet",
"//Source/common:SNTRule",
"//Source/common:SNTRuleIdentifiers",
"//Source/common:SNTStoredEvent",
"//Source/common:SNTStrengthify",
"//Source/common:SNTSystemInfo",
@@ -115,6 +117,7 @@ santa_unit_test(
"//Source/common:SNTFileInfo",
"//Source/common:SNTLogging",
"//Source/common:SNTRule",
"//Source/common:SNTRuleIdentifiers",
"//Source/common:SNTStoredEvent",
"//Source/common:SNTXPCBundleServiceInterface",
"//Source/common:SNTXPCControlInterface",
@@ -147,11 +150,27 @@ santa_unit_test(
],
)
santa_unit_test(
name = "SNTCommandRuleTest",
srcs = [
"Commands/SNTCommandRule.h",
"Commands/SNTCommandRuleTest.mm",
"SNTCommand.h",
"SNTCommandController.h",
],
deps = [
":santactl_lib",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTRule",
],
)
test_suite(
name = "unit_tests",
tests = [
":SNTCommandFileInfoTest",
":SNTCommandMetricsTest",
":SNTCommandRuleTest",
],
visibility = ["//:santa_package_group"],
)

View File

@@ -22,6 +22,7 @@
#import "Source/common/SNTFileInfo.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTRule.h"
#import "Source/common/SNTRuleIdentifiers.h"
#import "Source/common/SNTStoredEvent.h"
#import "Source/common/SNTXPCBundleServiceInterface.h"
#import "Source/common/SNTXPCControlInterface.h"
@@ -45,6 +46,7 @@ static NSString *const kSigningChain = @"Signing Chain";
static NSString *const kUniversalSigningChain = @"Universal Signing Chain";
static NSString *const kTeamID = @"Team ID";
static NSString *const kSigningID = @"Signing ID";
static NSString *const kCDHash = @"CDHash";
// signing chain keys
static NSString *const kCommonName = @"Common Name";
@@ -123,6 +125,7 @@ typedef id (^SNTAttributeBlock)(SNTCommandFileInfo *, SNTFileInfo *);
@property(readonly, copy, nonatomic) SNTAttributeBlock downloadAgent;
@property(readonly, copy, nonatomic) SNTAttributeBlock teamID;
@property(readonly, copy, nonatomic) SNTAttributeBlock signingID;
@property(readonly, copy, nonatomic) SNTAttributeBlock cdhash;
@property(readonly, copy, nonatomic) SNTAttributeBlock type;
@property(readonly, copy, nonatomic) SNTAttributeBlock pageZero;
@property(readonly, copy, nonatomic) SNTAttributeBlock codeSigned;
@@ -201,8 +204,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
+ (NSArray<NSString *> *)fileInfoKeys {
return @[
kPath, kSHA256, kSHA1, kBundleName, kBundleVersion, kBundleVersionStr, kDownloadReferrerURL,
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kSigningID, kType, kPageZero,
kCodeSigned, kRule, kSigningChain, kUniversalSigningChain
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kSigningID, kCDHash, kType,
kPageZero, kCodeSigned, kRule, kSigningChain, kUniversalSigningChain
];
}
@@ -236,6 +239,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
kUniversalSigningChain : self.universalSigningChain,
kTeamID : self.teamID,
kSigningID : self.signingID,
kCDHash : self.cdhash,
};
_printQueue = dispatch_queue_create("com.google.santactl.print_queue", DISPATCH_QUEUE_SERIAL);
@@ -376,33 +380,34 @@ REGISTER_COMMAND_NAME(@"fileinfo")
NSError *err;
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:&err];
NSString *teamID =
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier];
NSString *identifier =
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
NSString *cdhash = csc.cdhash;
NSString *teamID = csc.teamID;
NSString *identifier = csc.signingID;
NSString *signingID;
if (identifier) {
if (teamID) {
signingID = [NSString stringWithFormat:@"%@:%@", teamID, identifier];
} else {
id platformID =
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoPlatformIdentifier];
if ([platformID isKindOfClass:[NSNumber class]] && [platformID intValue] != 0) {
signingID = [NSString stringWithFormat:@"platform:%@", identifier];
}
} else if (csc.platformBinary) {
signingID = [NSString stringWithFormat:@"platform:%@", identifier];
}
}
[[cmd.daemonConn remoteObjectProxy] decisionForFilePath:fileInfo.path
fileSHA256:fileInfo.SHA256
certificateSHA256:err ? nil : csc.leafCertificate.SHA256
teamID:teamID
signingID:signingID
reply:^(SNTEventState s) {
state = s;
dispatch_semaphore_signal(sema);
}];
struct RuleIdentifiers identifiers = {
.cdhash = cdhash,
.binarySHA256 = fileInfo.SHA256,
.signingID = signingID,
.certificateSHA256 = err ? nil : csc.leafCertificate.SHA256,
.teamID = teamID,
};
[[cmd.daemonConn remoteObjectProxy]
decisionForFilePath:fileInfo.path
identifiers:[[SNTRuleIdentifiers alloc] initWithRuleIdentifiers:identifiers]
reply:^(SNTEventState s) {
state = s;
dispatch_semaphore_signal(sema);
}];
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
cmd.daemonUnavailable = YES;
return kCommunicationErrorMsg;
@@ -420,6 +425,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
case SNTEventStateAllowSigningID:
case SNTEventStateBlockSigningID: [output appendString:@" (SigningID)"]; break;
case SNTEventStateAllowCDHash:
case SNTEventStateBlockCDHash: [output appendString:@" (CDHash)"]; break;
case SNTEventStateAllowScope:
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
case SNTEventStateAllowCompiler: [output appendString:@" (Compiler)"]; break;
@@ -508,14 +515,30 @@ REGISTER_COMMAND_NAME(@"fileinfo")
- (SNTAttributeBlock)teamID {
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
return [csc.signingInformation valueForKey:@"teamid"];
return csc.teamID;
};
}
- (SNTAttributeBlock)signingID {
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
return [csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
NSString *identifier = csc.signingID;
NSString *teamID = csc.teamID;
if (!identifier) return nil;
if (teamID) {
return [NSString stringWithFormat:@"%@:%@", teamID, identifier];
} else if (csc.platformBinary) {
return [NSString stringWithFormat:@"platform:%@", identifier];
}
return nil;
};
}
- (SNTAttributeBlock)cdhash {
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
return csc.cdhash;
};
}

View File

@@ -67,7 +67,7 @@ REGISTER_COMMAND_NAME(@"printlog")
- (void)runWithArguments:(NSArray *)arguments {
JsonPrintOptions options;
options.always_print_enums_as_ints = false;
options.always_print_primitive_fields = true;
options.always_print_fields_with_no_presence = true;
options.preserve_proto_field_names = true;
options.add_whitespace = true;

View File

@@ -0,0 +1,20 @@
/// Copyright 2024 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <Foundation/Foundation.h>
#import "Source/santactl/SNTCommand.h"
@interface SNTCommandRule : SNTCommand <SNTCommandProtocol>
@end

View File

@@ -12,7 +12,9 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <CommonCrypto/CommonDigest.h>
#import <Foundation/Foundation.h>
#import <Kernel/kern/cs_blobs.h>
#import <MOLCertificate/MOLCertificate.h>
#import <MOLCodesignChecker/MOLCodesignChecker.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
@@ -22,13 +24,12 @@
#import "Source/common/SNTFileInfo.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTRule.h"
#import "Source/common/SNTRuleIdentifiers.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/santactl/Commands/SNTCommandRule.h"
#import "Source/santactl/SNTCommand.h"
#import "Source/santactl/SNTCommandController.h"
@interface SNTCommandRule : SNTCommand <SNTCommandProtocol>
@end
@implementation SNTCommandRule
REGISTER_COMMAND_NAME(@"rule")
@@ -46,58 +47,67 @@ REGISTER_COMMAND_NAME(@"rule")
}
+ (NSString *)longHelpText {
return (@"Usage: santactl rule [options]\n"
@" One of:\n"
@" --allow: add to allow\n"
@" --block: add to block\n"
@" --silent-block: add to silent block\n"
@" --compiler: allow and mark as a compiler\n"
@" --remove: remove existing rule\n"
@" --check: check for an existing rule\n"
@" --import {path}: import rules from a JSON file\n"
@" --export {path}: export rules to a JSON file\n"
@"\n"
@" One of:\n"
@" --path {path}: path of binary/bundle to add/remove.\n"
@" Will add the hash of the file currently at that path.\n"
@" Does not work with --check. Use the fileinfo verb to check.\n"
@" the rule state of a file.\n"
@" --identifier {sha256|teamID|signingID}: identifier to add/remove/check\n"
@" --sha256 {sha256}: hash to add/remove/check [deprecated]\n"
@"\n"
@" Optionally:\n"
@" --teamid: add or check a team ID rule instead of binary\n"
@" --signingid: add or check a signing ID rule instead of binary (see notes)\n"
@" --certificate: add or check a certificate sha256 rule instead of binary\n"
return (
@"Usage: santactl rule [options]\n"
@" One of:\n"
@" --allow: add to allow\n"
@" --block: add to block\n"
@" --silent-block: add to silent block\n"
@" --compiler: allow and mark as a compiler\n"
@" --remove: remove existing rule\n"
@" --check: check for an existing rule\n"
@" --import {path}: import rules from a JSON file\n"
@" --export {path}: export rules to a JSON file\n"
@"\n"
@" One of:\n"
@" --path {path}: path of binary/bundle to add/remove.\n"
@" Will add an appropriate rule for the file currently at that path.\n"
@" Defaults to a SHA-256 rule unless overridden with another flag.\n"
@" Does not work with --check. Use the fileinfo verb to check.\n"
@" the rule state of a file.\n"
@" --identifier {sha256|teamID|signingID|cdhash}: identifier to add/remove/check\n"
@" --sha256 {sha256}: hash to add/remove/check [deprecated]\n"
@"\n"
@" Optionally:\n"
@" --teamid: add or check a team ID rule instead of binary\n"
@" --signingid: add or check a signing ID rule instead of binary (see notes)\n"
@" --certificate: add or check a certificate sha256 rule instead of binary\n"
@" --cdhash: add or check a cdhash rule instead of binary\n"
#ifdef DEBUG
@" --force: allow manual changes even when SyncBaseUrl is set\n"
@" --force: allow manual changes even when SyncBaseUrl is set\n"
#endif
@" --message {message}: custom message\n"
@"\n"
@" Notes:\n"
@" The format of `identifier` when adding/checking a `signingid` rule is:\n"
@"\n"
@" `TeamID:SigningID`\n"
@"\n"
@" Because signing IDs are controlled by the binary author, this ensures\n"
@" that the signing ID is properly scoped to a developer. For the special\n"
@" case of platform binaries, `TeamID` should be replaced with the string\n"
@" \"platform\" (e.g. `platform:SigningID`). This allows for rules\n"
@" targeting Apple-signed binaries that do not have a team ID.\n"
@"\n"
@" Importing / Exporting Rules:\n"
@" If santa is not configured to use a sync server one can export\n"
@" & import its non-static rules to and from JSON files using the \n"
@" --export/--import flags. These files have the following form:\n"
@"\n"
@" {\"rules\": [{rule-dictionaries}]}\n"
@" e.g. {\"rules\": [\n"
@" {\"policy\": \"BLOCKLIST\",\n"
@" \"identifier\": "
@"\"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6\"\n"
@" \"custom_url\" : \"\",\n"
@" \"custom_msg\": \"/bin/ls block for demo\"}\n"
@" ]}\n");
@" --message {message}: custom message\n"
@" --clean: when importing rules via JSON clear all non-transitive rules before importing\n"
@" --clean-all: when importing rules via JSON clear all rules before importing\n"
@"\n"
@" Notes:\n"
@" The format of `identifier` when adding/checking a `signingid` rule is:\n"
@"\n"
@" `TeamID:SigningID`\n"
@"\n"
@" Because signing IDs are controlled by the binary author, this ensures\n"
@" that the signing ID is properly scoped to a developer. For the special\n"
@" case of platform binaries, `TeamID` should be replaced with the string\n"
@" \"platform\" (e.g. `platform:SigningID`). This allows for rules\n"
@" targeting Apple-signed binaries that do not have a team ID.\n"
@"\n"
@" Importing / Exporting Rules:\n"
@" If santa is not configured to use a sync server one can export\n"
@" & import its non-static rules to and from JSON files using the \n"
@" --export/--import flags. These files have the following form:\n"
@"\n"
@" {\"rules\": [{rule-dictionaries}]}\n"
@" e.g. {\"rules\": [\n"
@" {\"policy\": \"BLOCKLIST\",\n"
@" \"identifier\": "
@"\"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6\"\n"
@" \"custom_url\" : \"\",\n"
@" \"custom_msg\": \"/bin/ls block for demo\"}\n"
@" ]}\n"
@"\n"
@" By default rules are not cleared when importing. To clear the\n"
@" database you must use either --clean or --clean-all\n"
@"\n");
}
- (void)runWithArguments:(NSArray *)arguments {
@@ -121,6 +131,7 @@ REGISTER_COMMAND_NAME(@"rule")
NSString *path;
NSString *jsonFilePath;
BOOL check = NO;
SNTRuleCleanup cleanupType = SNTRuleCleanupNone;
BOOL importRules = NO;
BOOL exportRules = NO;
@@ -149,6 +160,8 @@ REGISTER_COMMAND_NAME(@"rule")
newRule.type = SNTRuleTypeTeamID;
} else if ([arg caseInsensitiveCompare:@"--signingid"] == NSOrderedSame) {
newRule.type = SNTRuleTypeSigningID;
} else if ([arg caseInsensitiveCompare:@"--cdhash"] == NSOrderedSame) {
newRule.type = SNTRuleTypeCDHash;
} else if ([arg caseInsensitiveCompare:@"--path"] == NSOrderedSame) {
if (++i > arguments.count - 1) {
[self printErrorUsageAndExit:@"--path requires an argument"];
@@ -182,6 +195,10 @@ REGISTER_COMMAND_NAME(@"rule")
[self printErrorUsageAndExit:@"--import requires an argument"];
}
jsonFilePath = arguments[i];
} else if ([arg caseInsensitiveCompare:@"--clean"] == NSOrderedSame) {
cleanupType = SNTRuleCleanupNonTransitive;
} else if ([arg caseInsensitiveCompare:@"--clean-all"] == NSOrderedSame) {
cleanupType = SNTRuleCleanupAll;
} else if ([arg caseInsensitiveCompare:@"--export"] == NSOrderedSame) {
if (importRules) {
[self printErrorUsageAndExit:@"--import and --export are mutually exclusive"];
@@ -200,12 +217,27 @@ REGISTER_COMMAND_NAME(@"rule")
}
}
if (!importRules && cleanupType != SNTRuleCleanupNone) {
switch (cleanupType) {
case SNTRuleCleanupNonTransitive:
[self printErrorUsageAndExit:@"--clean can only be used with --import"];
break;
case SNTRuleCleanupAll:
[self printErrorUsageAndExit:@"--clean-all can only be used with --import"];
break;
default:
// This is a programming error.
LOGE(@"Unexpected SNTRuleCleanupType %ld", cleanupType);
exit(EXIT_FAILURE);
}
}
if (jsonFilePath.length > 0) {
if (importRules) {
if (newRule.identifier != nil || path != nil || check) {
[self printErrorUsageAndExit:@"--import can only be used by itself"];
}
[self importJSONFile:jsonFilePath];
[self importJSONFile:jsonFilePath with:cleanupType];
} else if (exportRules) {
if (newRule.identifier != nil || path != nil || check) {
[self printErrorUsageAndExit:@"--export can only be used by itself"];
@@ -226,17 +258,36 @@ REGISTER_COMMAND_NAME(@"rule")
} else if (newRule.type == SNTRuleTypeCertificate) {
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
newRule.identifier = cs.leafCertificate.SHA256;
} else if (newRule.type == SNTRuleTypeTeamID || newRule.type == SNTRuleTypeSigningID) {
// noop
} else if (newRule.type == SNTRuleTypeCDHash) {
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
newRule.identifier = cs.cdhash;
} else if (newRule.type == SNTRuleTypeTeamID) {
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
newRule.identifier = cs.teamID;
} else if (newRule.type == SNTRuleTypeSigningID) {
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
if (cs.teamID.length) {
newRule.identifier = [NSString stringWithFormat:@"%@:%@", cs.teamID, cs.signingID];
} else if (cs.platformBinary) {
newRule.identifier = [NSString stringWithFormat:@"platform:%@", cs.signingID];
}
}
}
if (newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate) {
if (newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate ||
newRule.type == SNTRuleTypeCDHash) {
NSCharacterSet *nonHex =
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
if ([[newRule.identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length !=
64) {
NSUInteger length =
[[newRule.identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length;
if ((newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate) &&
length != CC_SHA256_DIGEST_LENGTH * 2) {
[self printErrorUsageAndExit:@"BINARY or CERTIFICATE rules require a valid SHA-256"];
} else if (newRule.type == SNTRuleTypeCDHash && length != CS_CDHASH_LEN * 2) {
[self printErrorUsageAndExit:
[NSString stringWithFormat:@"CDHASH rules require a valid hex string of length %d",
CS_CDHASH_LEN * 2]];
}
}
@@ -248,12 +299,13 @@ REGISTER_COMMAND_NAME(@"rule")
if (newRule.state == SNTRuleStateUnknown) {
[self printErrorUsageAndExit:@"No state specified"];
} else if (!newRule.identifier) {
[self printErrorUsageAndExit:@"Either SHA-256, team ID, or path to file must be specified"];
[self printErrorUsageAndExit:
@"A valid SHA-256, CDHash, Signing ID, team ID, or path to file must be specified"];
}
[[self.daemonConn remoteObjectProxy]
databaseRuleAddRules:@[ newRule ]
cleanSlate:NO
ruleCleanup:SNTRuleCleanupNone
reply:^(NSError *error) {
if (error) {
printf("Failed to modify rules: %s",
@@ -263,7 +315,7 @@ REGISTER_COMMAND_NAME(@"rule")
} else {
NSString *ruleType;
switch (newRule.type) {
case SNTRuleTypeCertificate:
case SNTRuleTypeCertificate: ruleType = @"Certificate SHA-256"; break;
case SNTRuleTypeBinary: {
ruleType = @"SHA-256";
break;
@@ -272,6 +324,14 @@ REGISTER_COMMAND_NAME(@"rule")
ruleType = @"Team ID";
break;
}
case SNTRuleTypeSigningID: {
ruleType = @"Signing ID";
break;
}
case SNTRuleTypeCDHash: {
ruleType = @"CDHash";
break;
}
default: ruleType = @"(Unknown type)";
}
if (newRule.state == SNTRuleStateRemove) {
@@ -286,79 +346,109 @@ REGISTER_COMMAND_NAME(@"rule")
}];
}
// IMPORTANT: This method makes no attempt to validate whether or not the data
// in a rule is valid. It merely constructs a string with the given data.
// E.g., TeamID compiler rules are not currently supproted, but if a test rule
// is provided with that state, an appropriate string will be returned.
+ (NSString *)stringifyRule:(SNTRule *)rule withColor:(BOOL)colorize {
NSMutableString *output;
// Rule state is saved as eventState for output colorization down below
SNTEventState eventState = SNTEventStateUnknown;
switch (rule.state) {
case SNTRuleStateUnknown:
output = [@"No rule exists with the given parameters" mutableCopy];
break;
case SNTRuleStateAllow: OS_FALLTHROUGH;
case SNTRuleStateAllowCompiler: OS_FALLTHROUGH;
case SNTRuleStateAllowTransitive:
output = [@"Allowed" mutableCopy];
eventState = SNTEventStateAllow;
break;
case SNTRuleStateBlock: OS_FALLTHROUGH;
case SNTRuleStateSilentBlock:
output = [@"Blocked" mutableCopy];
eventState = SNTEventStateBlock;
break;
case SNTRuleStateRemove: OS_FALLTHROUGH;
default:
output = [NSMutableString stringWithFormat:@"Unexpected rule state: %ld", rule.state];
break;
}
if (rule.state == SNTRuleStateUnknown) {
// No more output to append
return output;
}
[output appendString:@" ("];
switch (rule.type) {
case SNTRuleTypeUnknown: [output appendString:@"Unknown"]; break;
case SNTRuleTypeCDHash: [output appendString:@"CDHash"]; break;
case SNTRuleTypeBinary: [output appendString:@"Binary"]; break;
case SNTRuleTypeSigningID: [output appendString:@"SigningID"]; break;
case SNTRuleTypeCertificate: [output appendString:@"Certificate"]; break;
case SNTRuleTypeTeamID: [output appendString:@"TeamID"]; break;
default:
output = [NSMutableString stringWithFormat:@"Unexpected rule type: %ld", rule.type];
break;
}
// Add additional attributes
switch (rule.state) {
case SNTRuleStateAllowCompiler: [output appendString:@", Compiler"]; break;
case SNTRuleStateAllowTransitive: [output appendString:@", Transitive"]; break;
case SNTRuleStateSilentBlock: [output appendString:@", Silent"]; break;
default: break;
}
[output appendString:@")"];
// Colorize
if (colorize) {
if ((SNTEventStateAllow & eventState)) {
[output insertString:@"\033[32m" atIndex:0];
[output appendString:@"\033[0m"];
} else if ((SNTEventStateBlock & eventState)) {
[output insertString:@"\033[31m" atIndex:0];
[output appendString:@"\033[0m"];
} else {
[output insertString:@"\033[33m" atIndex:0];
[output appendString:@"\033[0m"];
}
}
if (rule.state == SNTRuleStateAllowTransitive) {
NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:rule.timestamp];
[output appendString:[NSString stringWithFormat:@"\nlast access date: %@", [date description]]];
}
return output;
}
- (void)printStateOfRule:(SNTRule *)rule daemonConnection:(MOLXPCConnection *)daemonConn {
id<SNTDaemonControlXPC> rop = [daemonConn synchronousRemoteObjectProxy];
NSString *fileSHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil;
NSString *certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil;
NSString *teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil;
NSString *signingID = (rule.type == SNTRuleTypeSigningID) ? rule.identifier : nil;
__block NSMutableString *output;
[rop decisionForFilePath:nil
fileSHA256:fileSHA256
certificateSHA256:certificateSHA256
teamID:teamID
signingID:signingID
reply:^(SNTEventState s) {
output =
(SNTEventStateAllow & s) ? @"Allowed".mutableCopy : @"Blocked".mutableCopy;
switch (s) {
case SNTEventStateAllowUnknown:
case SNTEventStateBlockUnknown: [output appendString:@" (Unknown)"]; break;
case SNTEventStateAllowBinary:
case SNTEventStateBlockBinary: [output appendString:@" (Binary)"]; break;
case SNTEventStateAllowCertificate:
case SNTEventStateBlockCertificate:
[output appendString:@" (Certificate)"];
break;
case SNTEventStateAllowScope:
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
case SNTEventStateAllowCompiler:
[output appendString:@" (Compiler)"];
break;
case SNTEventStateAllowTransitive:
[output appendString:@" (Transitive)"];
break;
case SNTEventStateAllowTeamID:
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
case SNTEventStateAllowSigningID:
case SNTEventStateBlockSigningID:
[output appendString:@" (SigningID)"];
break;
default: output = @"None".mutableCopy; break;
}
if (isatty(STDOUT_FILENO)) {
if ((SNTEventStateAllow & s)) {
[output insertString:@"\033[32m" atIndex:0];
[output appendString:@"\033[0m"];
} else if ((SNTEventStateBlock & s)) {
[output insertString:@"\033[31m" atIndex:0];
[output appendString:@"\033[0m"];
} else {
[output insertString:@"\033[33m" atIndex:0];
[output appendString:@"\033[0m"];
}
}
}];
__block NSString *output;
[rop databaseRuleForBinarySHA256:fileSHA256
certificateSHA256:certificateSHA256
teamID:teamID
signingID:signingID
reply:^(SNTRule *r) {
if (r.state == SNTRuleStateAllowTransitive) {
NSDate *date =
[NSDate dateWithTimeIntervalSinceReferenceDate:r.timestamp];
[output appendString:[NSString
stringWithFormat:@"\nlast access date: %@",
[date description]]];
}
}];
struct RuleIdentifiers identifiers = {
.cdhash = (rule.type == SNTRuleTypeCDHash) ? rule.identifier : nil,
.binarySHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil,
.certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil,
.teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil,
.signingID = (rule.type == SNTRuleTypeSigningID) ? rule.identifier : nil,
};
[rop databaseRuleForIdentifiers:[[SNTRuleIdentifiers alloc] initWithRuleIdentifiers:identifiers]
reply:^(SNTRule *r) {
output = [SNTCommandRule stringifyRule:r
withColor:(isatty(STDOUT_FILENO) == 1)];
}];
printf("%s\n", output.UTF8String);
exit(0);
}
- (void)importJSONFile:(NSString *)jsonFilePath {
- (void)importJSONFile:(NSString *)jsonFilePath with:(SNTRuleCleanup)cleanupType {
// If the file exists parse it and then add the rules one at a time.
NSError *error;
NSData *data = [NSData dataWithContentsOfFile:jsonFilePath options:0 error:&error];
@@ -395,7 +485,7 @@ REGISTER_COMMAND_NAME(@"rule")
[[self.daemonConn remoteObjectProxy]
databaseRuleAddRules:parsedRules
cleanSlate:NO
ruleCleanup:cleanupType
reply:^(NSError *error) {
if (error) {
printf("Failed to modify rules: %s",
@@ -425,7 +515,7 @@ REGISTER_COMMAND_NAME(@"rule")
NSMutableArray *rulesAsDicts = [[NSMutableArray alloc] init];
for (SNTRule *rule in rules) {
// Omit transitive and remove rules as they're not relevan.
// Omit transitive and remove rules as they're not relevant.
if (rule.state == SNTRuleStateAllowTransitive || rule.state == SNTRuleStateRemove) {
continue;
}

View File

@@ -0,0 +1,96 @@
/// Copyright 2024 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#include <map>
#include <utility>
#import "Source/common/SNTRule.h"
#import "Source/santactl/Commands/SNTCommandRule.h"
@interface SNTCommandRule (Testing)
+ (NSString *)stringifyRule:(SNTRule *)rule withColor:(BOOL)colorize;
@end
@interface SNTRule ()
@property(readwrite) NSUInteger timestamp;
@end
@interface SNTCommandRuleTest : XCTestCase
@end
@implementation SNTCommandRuleTest
- (void)testStringifyRule {
std::map<std::pair<SNTRuleType, SNTRuleState>, NSString *> ruleCheckToString = {
{{SNTRuleTypeUnknown, SNTRuleStateUnknown}, @"No rule exists with the given parameters"},
{{SNTRuleTypeUnknown, SNTRuleStateAllow}, @"Allowed (Unknown)"},
{{SNTRuleTypeUnknown, SNTRuleStateBlock}, @"Blocked (Unknown)"},
{{SNTRuleTypeUnknown, SNTRuleStateSilentBlock}, @"Blocked (Unknown, Silent)"},
{{SNTRuleTypeUnknown, SNTRuleStateRemove}, @"Unexpected rule state: 4 (Unknown)"},
{{SNTRuleTypeUnknown, SNTRuleStateAllowCompiler}, @"Allowed (Unknown, Compiler)"},
{{SNTRuleTypeUnknown, SNTRuleStateAllowTransitive},
@"Allowed (Unknown, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"},
{{SNTRuleTypeBinary, SNTRuleStateUnknown}, @"No rule exists with the given parameters"},
{{SNTRuleTypeBinary, SNTRuleStateAllow}, @"Allowed (Binary)"},
{{SNTRuleTypeBinary, SNTRuleStateBlock}, @"Blocked (Binary)"},
{{SNTRuleTypeBinary, SNTRuleStateSilentBlock}, @"Blocked (Binary, Silent)"},
{{SNTRuleTypeBinary, SNTRuleStateRemove}, @"Unexpected rule state: 4 (Binary)"},
{{SNTRuleTypeBinary, SNTRuleStateAllowCompiler}, @"Allowed (Binary, Compiler)"},
{{SNTRuleTypeBinary, SNTRuleStateAllowTransitive},
@"Allowed (Binary, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"},
{{SNTRuleTypeSigningID, SNTRuleStateUnknown}, @"No rule exists with the given parameters"},
{{SNTRuleTypeSigningID, SNTRuleStateAllow}, @"Allowed (SigningID)"},
{{SNTRuleTypeSigningID, SNTRuleStateBlock}, @"Blocked (SigningID)"},
{{SNTRuleTypeSigningID, SNTRuleStateSilentBlock}, @"Blocked (SigningID, Silent)"},
{{SNTRuleTypeSigningID, SNTRuleStateRemove}, @"Unexpected rule state: 4 (SigningID)"},
{{SNTRuleTypeSigningID, SNTRuleStateAllowCompiler}, @"Allowed (SigningID, Compiler)"},
{{SNTRuleTypeSigningID, SNTRuleStateAllowTransitive},
@"Allowed (SigningID, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"},
{{SNTRuleTypeCertificate, SNTRuleStateUnknown}, @"No rule exists with the given parameters"},
{{SNTRuleTypeCertificate, SNTRuleStateAllow}, @"Allowed (Certificate)"},
{{SNTRuleTypeCertificate, SNTRuleStateBlock}, @"Blocked (Certificate)"},
{{SNTRuleTypeCertificate, SNTRuleStateSilentBlock}, @"Blocked (Certificate, Silent)"},
{{SNTRuleTypeCertificate, SNTRuleStateRemove}, @"Unexpected rule state: 4 (Certificate)"},
{{SNTRuleTypeCertificate, SNTRuleStateAllowCompiler}, @"Allowed (Certificate, Compiler)"},
{{SNTRuleTypeCertificate, SNTRuleStateAllowTransitive},
@"Allowed (Certificate, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"},
{{SNTRuleTypeTeamID, SNTRuleStateUnknown}, @"No rule exists with the given parameters"},
{{SNTRuleTypeTeamID, SNTRuleStateAllow}, @"Allowed (TeamID)"},
{{SNTRuleTypeTeamID, SNTRuleStateBlock}, @"Blocked (TeamID)"},
{{SNTRuleTypeTeamID, SNTRuleStateSilentBlock}, @"Blocked (TeamID, Silent)"},
{{SNTRuleTypeTeamID, SNTRuleStateRemove}, @"Unexpected rule state: 4 (TeamID)"},
{{SNTRuleTypeTeamID, SNTRuleStateAllowCompiler}, @"Allowed (TeamID, Compiler)"},
{{SNTRuleTypeTeamID, SNTRuleStateAllowTransitive},
@"Allowed (TeamID, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"},
};
SNTRule *rule = [[SNTRule alloc] init];
rule.timestamp = 700000000; // time interval since reference date
for (const auto &[typeAndState, want] : ruleCheckToString) {
rule.type = typeAndState.first;
rule.state = typeAndState.second;
NSString *got = [SNTCommandRule stringifyRule:rule withColor:NO];
XCTAssertEqualObjects(got, want);
}
}
@end

View File

@@ -56,7 +56,6 @@ REGISTER_COMMAND_NAME(@"status")
}
- (void)runWithArguments:(NSArray *)arguments {
dispatch_group_t group = dispatch_group_create();
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
// Daemon status
@@ -92,22 +91,20 @@ REGISTER_COMMAND_NAME(@"status")
}];
// Database counts
__block int64_t eventCount = -1;
__block int64_t binaryRuleCount = -1;
__block int64_t certRuleCount = -1;
__block int64_t teamIDRuleCount = -1;
__block int64_t signingIDRuleCount = -1;
__block int64_t compilerRuleCount = -1;
__block int64_t transitiveRuleCount = -1;
[rop databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler,
int64_t transitive, int64_t teamID, int64_t signingID) {
binaryRuleCount = binary;
certRuleCount = certificate;
teamIDRuleCount = teamID;
signingIDRuleCount = signingID;
compilerRuleCount = compiler;
transitiveRuleCount = transitive;
__block struct RuleCounts ruleCounts = {
.binary = -1,
.certificate = -1,
.compiler = -1,
.transitive = -1,
.teamID = -1,
.signingID = -1,
.cdhash = -1,
};
[rop databaseRuleCounts:^(struct RuleCounts counts) {
ruleCounts = counts;
}];
__block int64_t eventCount = -1;
[rop databaseEventCount:^(int64_t count) {
eventCount = count;
}];
@@ -130,8 +127,8 @@ REGISTER_COMMAND_NAME(@"status")
}];
__block BOOL syncCleanReqd = NO;
[rop syncCleanRequired:^(BOOL clean) {
syncCleanReqd = clean;
[rop syncTypeRequired:^(SNTSyncType syncType) {
syncCleanReqd = (syncType == SNTSyncTypeClean || syncType == SNTSyncTypeCleanAll);
}];
__block BOOL pushNotifications = NO;
@@ -169,10 +166,15 @@ REGISTER_COMMAND_NAME(@"status")
}
}];
// Wait a maximum of 5s for stats collected from daemon to arrive.
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5))) {
fprintf(stderr, "Failed to retrieve some stats from daemon\n\n");
}
__block BOOL blockUSBMount = NO;
[rop blockUSBMount:^(BOOL response) {
blockUSBMount = response;
}];
__block NSArray<NSString *> *remountUSBMode;
[rop remountUSBMode:^(NSArray<NSString *> *response) {
remountUSBMode = response;
}];
// Format dates
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
@@ -196,25 +198,25 @@ REGISTER_COMMAND_NAME(@"status")
@"daemon" : @{
@"driver_connected" : @(YES),
@"mode" : clientMode ?: @"null",
@"transitive_rules" : @(enableTransitiveRules),
@"log_type" : eventLogType,
@"file_logging" : @(fileLogging),
@"watchdog_cpu_events" : @(cpuEvents),
@"watchdog_ram_events" : @(ramEvents),
@"watchdog_cpu_peak" : @(cpuPeak),
@"watchdog_ram_peak" : @(ramPeak),
@"block_usb" : @(configurator.blockUSBMount),
@"remount_usb_mode" : (configurator.blockUSBMount && configurator.remountUSBMode.count
? configurator.remountUSBMode
: @""),
@"block_usb" : @(blockUSBMount),
@"remount_usb_mode" : (blockUSBMount && remountUSBMode.count ? remountUSBMode : @""),
@"on_start_usb_options" : StartupOptionToString(configurator.onStartUSBOptions),
},
@"database" : @{
@"binary_rules" : @(binaryRuleCount),
@"certificate_rules" : @(certRuleCount),
@"teamid_rules" : @(teamIDRuleCount),
@"signingid_rules" : @(signingIDRuleCount),
@"compiler_rules" : @(compilerRuleCount),
@"transitive_rules" : @(transitiveRuleCount),
@"binary_rules" : @(ruleCounts.binary),
@"certificate_rules" : @(ruleCounts.certificate),
@"teamid_rules" : @(ruleCounts.teamID),
@"signingid_rules" : @(ruleCounts.signingID),
@"cdhash_rules" : @(ruleCounts.cdhash),
@"compiler_rules" : @(ruleCounts.compiler),
@"transitive_rules" : @(ruleCounts.transitive),
@"events_pending_upload" : @(eventCount),
},
@"static_rules" : @{
@@ -227,7 +229,6 @@ REGISTER_COMMAND_NAME(@"status")
@"last_successful_rule" : ruleSyncLastSuccessStr ?: @"null",
@"push_notifications" : pushNotifications ? @"Connected" : @"Disconnected",
@"bundle_scanning" : @(enableBundles),
@"transitive_rules" : @(enableTransitiveRules),
},
} mutableCopy];
@@ -260,12 +261,17 @@ REGISTER_COMMAND_NAME(@"status")
} else {
printf(">>> Daemon Info\n");
printf(" %-25s | %s\n", "Mode", [clientMode UTF8String]);
if (enableTransitiveRules) {
printf(" %-25s | %s\n", "Transitive Rules", (enableTransitiveRules ? "Yes" : "No"));
}
printf(" %-25s | %s\n", "Log Type", [eventLogType UTF8String]);
printf(" %-25s | %s\n", "File Logging", (fileLogging ? "Yes" : "No"));
printf(" %-25s | %s\n", "USB Blocking", (configurator.blockUSBMount ? "Yes" : "No"));
if (configurator.blockUSBMount && configurator.remountUSBMode.count > 0) {
printf(" %-25s | %s\n", "USB Blocking", (blockUSBMount ? "Yes" : "No"));
if (blockUSBMount && remountUSBMode.count > 0) {
printf(" %-25s | %s\n", "USB Remounting Mode",
[[configurator.remountUSBMode componentsJoinedByString:@", "] UTF8String]);
[[remountUSBMode componentsJoinedByString:@", "] UTF8String]);
}
printf(" %-25s | %s\n", "On Start USB Options",
StartupOptionToString(configurator.onStartUSBOptions).UTF8String);
@@ -277,12 +283,13 @@ REGISTER_COMMAND_NAME(@"status")
printf(" %-25s | %lld\n", "Non-root cache count", nonRootCacheCount);
printf(">>> Database Info\n");
printf(" %-25s | %lld\n", "Binary Rules", binaryRuleCount);
printf(" %-25s | %lld\n", "Certificate Rules", certRuleCount);
printf(" %-25s | %lld\n", "TeamID Rules", teamIDRuleCount);
printf(" %-25s | %lld\n", "SigningID Rules", signingIDRuleCount);
printf(" %-25s | %lld\n", "Compiler Rules", compilerRuleCount);
printf(" %-25s | %lld\n", "Transitive Rules", transitiveRuleCount);
printf(" %-25s | %lld\n", "Binary Rules", ruleCounts.binary);
printf(" %-25s | %lld\n", "Certificate Rules", ruleCounts.certificate);
printf(" %-25s | %lld\n", "TeamID Rules", ruleCounts.teamID);
printf(" %-25s | %lld\n", "SigningID Rules", ruleCounts.signingID);
printf(" %-25s | %lld\n", "CDHash Rules", ruleCounts.cdhash);
printf(" %-25s | %lld\n", "Compiler Rules", ruleCounts.compiler);
printf(" %-25s | %lld\n", "Transitive Rules", ruleCounts.transitive);
printf(" %-25s | %lld\n", "Events Pending Upload", eventCount);
if ([SNTConfigurator configurator].staticRules.count) {
@@ -308,7 +315,6 @@ REGISTER_COMMAND_NAME(@"status")
printf(" %-25s | %s\n", "Push Notifications",
(pushNotifications ? "Connected" : "Disconnected"));
printf(" %-25s | %s\n", "Bundle Scanning", (enableBundles ? "Yes" : "No"));
printf(" %-25s | %s\n", "Transitive Rules", (enableTransitiveRules ? "Yes" : "No"));
}
if (exportMetrics) {

View File

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

View File

@@ -21,6 +21,9 @@ objc_library(
name = "SNTRuleTable",
srcs = ["DataLayer/SNTRuleTable.m"],
hdrs = ["DataLayer/SNTRuleTable.h"],
sdk_dylibs = [
"EndpointSecurity",
],
deps = [
":SNTDatabaseTable",
"//Source/common:Platform",
@@ -30,6 +33,7 @@ objc_library(
"//Source/common:SNTFileInfo",
"//Source/common:SNTLogging",
"//Source/common:SNTRule",
"//Source/common:SNTRuleIdentifiers",
"@MOLCertificate",
"@MOLCodesignChecker",
],
@@ -189,23 +193,35 @@ objc_library(
objc_library(
name = "SNTPolicyProcessor",
srcs = [
"DataLayer/SNTDatabaseTable.h",
"DataLayer/SNTRuleTable.h",
"SNTPolicyProcessor.m",
],
srcs = ["SNTPolicyProcessor.mm"],
hdrs = ["SNTPolicyProcessor.h"],
deps = [
":SNTRuleTable",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTDeepCopy",
"//Source/common:SNTFileInfo",
"//Source/common:SNTLogging",
"//Source/common:SNTRule",
"//Source/common:SNTRuleIdentifiers",
"@FMDB",
"@MOLCertificate",
"@MOLCodesignChecker",
"@MOLXPCConnection",
"@com_google_absl//absl/container:flat_hash_map",
],
)
santa_unit_test(
name = "SNTPolicyProcessorTest",
srcs = ["SNTPolicyProcessorTest.mm"],
deps = [
":SNTPolicyProcessor",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTConfigurator",
"//Source/common:SNTRule",
"//Source/common:TestUtils",
],
)
@@ -236,10 +252,12 @@ objc_library(
":SNTSyncdQueue",
":TTYWriter",
"//Source/common:BranchPrediction",
"//Source/common:PrefixTree",
"//Source/common:SNTBlockMessage",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTDeepCopy",
"//Source/common:SNTDropRootPrivs",
"//Source/common:SNTFileInfo",
"//Source/common:SNTLogging",
@@ -248,7 +266,9 @@ objc_library(
"//Source/common:SNTStoredEvent",
"//Source/common:SantaVnode",
"//Source/common:String",
"//Source/common:Unit",
"@MOLCodesignChecker",
"@com_google_absl//absl/synchronization",
],
)
@@ -278,12 +298,27 @@ objc_library(
":SNTEndpointSecurityClientBase",
":WatchItemPolicy",
"//Source/common:BranchPrediction",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTLogging",
"//Source/common:SystemResources",
],
)
objc_library(
name = "SNTEndpointSecurityTreeAwareClient",
srcs = ["EventProviders/SNTEndpointSecurityTreeAwareClient.mm"],
hdrs = ["EventProviders/SNTEndpointSecurityTreeAwareClient.h"],
deps = [
":EndpointSecurityAPI",
":EndpointSecurityMessage",
":Metrics",
":SNTEndpointSecurityClient",
"//Source/santad/ProcessTree:SNTEndpointSecurityAdapter",
"//Source/santad/ProcessTree:process_tree",
],
)
objc_library(
name = "SNTEndpointSecurityRecorder",
srcs = ["EventProviders/SNTEndpointSecurityRecorder.mm"],
@@ -297,13 +332,14 @@ objc_library(
":EndpointSecurityMessage",
":Metrics",
":SNTCompilerController",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
":SNTEndpointSecurityTreeAwareClient",
"//Source/common:PrefixTree",
"//Source/common:SNTConfigurator",
"//Source/common:SNTLogging",
"//Source/common:String",
"//Source/common:Unit",
"//Source/santad/ProcessTree:process_tree",
],
)
@@ -437,6 +473,8 @@ objc_library(
":EndpointSecurityEnrichedTypes",
"//Source/common:SNTLogging",
"//Source/common:SantaCache",
"//Source/santad/ProcessTree:SNTEndpointSecurityAdapter",
"//Source/santad/ProcessTree:process_tree",
],
)
@@ -445,6 +483,7 @@ objc_library(
hdrs = ["EventProviders/EndpointSecurity/EnrichedTypes.h"],
deps = [
":EndpointSecurityMessage",
"//Source/santad/ProcessTree:process_tree_cc_proto",
],
)
@@ -470,7 +509,6 @@ objc_library(
"bsm",
],
deps = [
":EndpointSecurityEnrichedTypes",
":EndpointSecurityMessage",
":SNTDecisionCache",
"//Source/common:SantaCache",
@@ -620,6 +658,8 @@ objc_library(
deps = [
":EndpointSecurityClient",
":WatchItemPolicy",
"//Source/common:SNTCommonEnums",
"//Source/santad/ProcessTree:process_tree",
],
)
@@ -632,6 +672,9 @@ objc_library(
name = "EndpointSecurityAPI",
srcs = ["EventProviders/EndpointSecurity/EndpointSecurityAPI.mm"],
hdrs = ["EventProviders/EndpointSecurity/EndpointSecurityAPI.h"],
sdk_dylibs = [
"EndpointSecurity",
],
deps = [
":EndpointSecurityClient",
":EndpointSecurityMessage",
@@ -660,6 +703,7 @@ objc_library(
"//Source/common:SNTLogging",
"//Source/common:SNTMetricSet",
"//Source/common:SNTRule",
"//Source/common:SNTRuleIdentifiers",
"//Source/common:SNTStoredEvent",
"//Source/common:SNTStrengthify",
"//Source/common:SNTXPCControlInterface",
@@ -675,6 +719,7 @@ objc_library(
srcs = ["Metrics.mm"],
hdrs = ["Metrics.h"],
deps = [
":EndpointSecurityMessage",
":SNTApplicationCoreMetrics",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTLogging",
@@ -716,6 +761,7 @@ objc_library(
"//Source/common:SNTXPCNotifierInterface",
"//Source/common:SNTXPCSyncServiceInterface",
"//Source/common:Unit",
"//Source/santad/ProcessTree:process_tree",
"@MOLXPCConnection",
],
)
@@ -747,6 +793,8 @@ objc_library(
"//Source/common:SNTXPCControlInterface",
"//Source/common:SNTXPCUnprivilegedControlInterface",
"//Source/common:Unit",
"//Source/santad/ProcessTree:process_tree",
"//Source/santad/ProcessTree/annotations:originator",
"@MOLXPCConnection",
],
)
@@ -869,18 +917,17 @@ santa_unit_test(
"//Source/common:SNTFileInfo",
"//Source/common:SNTLogging",
"//Source/common:SNTRule",
"//Source/common:SNTRuleIdentifiers",
"@FMDB",
"@MOLCertificate",
"@MOLCodesignChecker",
"@OCMock",
],
)
santa_unit_test(
name = "SantadTest",
srcs = ["SantadTest.mm"],
data = [
"//Source/santad/testdata:binaryrules_testdata",
],
minimum_os_version = "11.0",
sdk_dylibs = [
"bsm",
@@ -889,6 +936,9 @@ santa_unit_test(
sdk_frameworks = [
"DiskArbitration",
],
structured_resources = [
"//Source/santad/testdata:binaryrules_testdata",
],
tags = ["exclusive"],
deps = [
":EndpointSecurityMessage",
@@ -897,6 +947,7 @@ santa_unit_test(
":SNTDatabaseController",
":SNTDecisionCache",
":SNTEndpointSecurityAuthorizer",
":SNTEndpointSecurityClient",
":SantadDeps",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTConfigurator",
@@ -979,7 +1030,7 @@ santa_unit_test(
santa_unit_test(
name = "EndpointSecuritySerializerProtobufTest",
srcs = ["Logs/EndpointSecurity/Serializers/ProtobufTest.mm"],
data = [
structured_resources = [
"//Source/santad/testdata:protobuf_json_testdata",
],
deps = [
@@ -1134,7 +1185,10 @@ santa_unit_test(
name = "MetricsTest",
srcs = ["MetricsTest.mm"],
deps = [
":EndpointSecurityMessage",
":Metrics",
":MockEndpointSecurityAPI",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTMetricSet",
"//Source/common:TestUtils",
"@OCMock",
@@ -1154,6 +1208,7 @@ santa_unit_test(
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTRule",
"//Source/common:SantaVnode",
"//Source/common:TestUtils",
"@OCMock",
],
@@ -1174,7 +1229,9 @@ santa_unit_test(
":MockEndpointSecurityAPI",
":SNTEndpointSecurityClient",
":WatchItemPolicy",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SystemResources",
"//Source/common:TestUtils",
"@OCMock",
"@com_google_googletest//:gtest",
@@ -1202,6 +1259,7 @@ santa_unit_test(
"//Source/common:SNTFileInfo",
"//Source/common:SNTMetricSet",
"//Source/common:SNTRule",
"//Source/common:SNTRuleIdentifiers",
"//Source/common:TestUtils",
"@MOLCertificate",
"@MOLCodesignChecker",
@@ -1318,6 +1376,7 @@ santa_unit_test(
":EndpointSecurityMessage",
":Metrics",
":MockEndpointSecurityAPI",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityDeviceManager",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
@@ -1376,10 +1435,13 @@ test_suite(
":SNTEndpointSecurityTamperResistanceTest",
":SNTEventTableTest",
":SNTExecutionControllerTest",
":SNTPolicyProcessorTest",
":SNTRuleTableTest",
":SantadTest",
":WatchItemsTest",
"//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:fsspool_test",
"//Source/santad/ProcessTree:process_tree_test",
"//Source/santad/ProcessTree/annotations:originator_test",
],
visibility = ["//:santa_package_group"],
)

View File

@@ -15,6 +15,7 @@
#import <Foundation/Foundation.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTRuleIdentifiers.h"
#import "Source/santad/DataLayer/SNTDatabaseTable.h"
@class SNTCachedDecision;
@@ -29,57 +30,60 @@
///
/// @return Number of rules in the database
///
- (NSUInteger)ruleCount;
- (int64_t)ruleCount;
///
/// @return Number of binary rules in the database
///
- (NSUInteger)binaryRuleCount;
- (int64_t)binaryRuleCount;
///
/// @return Number of compiler rules in the database
///
- (NSUInteger)compilerRuleCount;
- (int64_t)compilerRuleCount;
///
/// @return Number of transitive rules in the database
///
- (NSUInteger)transitiveRuleCount;
- (int64_t)transitiveRuleCount;
///
/// @return Number of certificate rules in the database
///
- (NSUInteger)certificateRuleCount;
- (int64_t)certificateRuleCount;
///
/// @return Number of team ID rules in the database
///
- (NSUInteger)teamIDRuleCount;
- (int64_t)teamIDRuleCount;
///
/// @return Number of signing ID rules in the database
///
- (NSUInteger)signingIDRuleCount;
- (int64_t)signingIDRuleCount;
///
/// @return Rule for binary, signingID, certificate or teamID (in that order).
/// @return Number of cdhash rules in the database
///
- (int64_t)cdhashRuleCount;
///
/// @return Rule for given identifiers.
/// Currently: binary, signingID, certificate or teamID (in that order).
/// The first matching rule found is returned.
///
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
signingID:(NSString *)signingID
certificateSHA256:(NSString *)certificateSHA256
teamID:(NSString *)teamID;
- (SNTRule *)ruleForIdentifiers:(struct RuleIdentifiers)identifiers;
///
/// Add an array of rules to the database. The rules will be added within a transaction and the
/// transaction will abort if any rule fails to add.
///
/// @param rules Array of SNTRule's to add.
/// @param cleanSlate If true, remove all rules before adding the new rules.
/// @param ruleCleanup Rule cleanup type to perform (e.g. all, none, non-transitive).
/// @param error When returning NO, will be filled with appropriate error.
/// @return YES if adding all rules passed, NO if any were rejected.
///
- (BOOL)addRules:(NSArray *)rules cleanSlate:(BOOL)cleanSlate error:(NSError **)error;
- (BOOL)addRules:(NSArray *)rules ruleCleanup:(SNTRuleCleanup)cleanupType error:(NSError **)error;
///
/// Checks the given array of rules to see if adding any of them to the rules database would

View File

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

View File

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

View File

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

View File

@@ -31,6 +31,10 @@ static NSString *const kFlushCacheReasonRulesChanged = @"RulesChanged";
static NSString *const kFlushCacheReasonStaticRulesChanged = @"StaticRulesChanged";
static NSString *const kFlushCacheReasonExplicitCommand = @"ExplicitCommand";
static NSString *const kFlushCacheReasonFilesystemUnmounted = @"FilesystemUnmounted";
static NSString *const kFlushCacheReasonEntitlementsPrefixFilterChanged =
@"EntitlementsPrefixFilterChanged";
static NSString *const kFlushCacheReasonEntitlementsTeamIDFilterChanged =
@"EntitlementsTeamIDFilterChanged";
namespace santa::santad::event_providers {
@@ -59,6 +63,10 @@ NSString *const FlushCacheReasonToString(FlushCacheReason reason) {
case FlushCacheReason::kStaticRulesChanged: return kFlushCacheReasonStaticRulesChanged;
case FlushCacheReason::kExplicitCommand: return kFlushCacheReasonExplicitCommand;
case FlushCacheReason::kFilesystemUnmounted: return kFlushCacheReasonFilesystemUnmounted;
case FlushCacheReason::kEntitlementsPrefixFilterChanged:
return kFlushCacheReasonEntitlementsPrefixFilterChanged;
case FlushCacheReason::kEntitlementsTeamIDFilterChanged:
return kFlushCacheReasonEntitlementsTeamIDFilterChanged;
default:
[NSException raise:@"Invalid reason"
format:@"Unknown reason value: %d", static_cast<int>(reason)];

View File

@@ -230,13 +230,16 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
{FlushCacheReason::kStaticRulesChanged, @"StaticRulesChanged"},
{FlushCacheReason::kExplicitCommand, @"ExplicitCommand"},
{FlushCacheReason::kFilesystemUnmounted, @"FilesystemUnmounted"},
{FlushCacheReason::kEntitlementsPrefixFilterChanged, @"EntitlementsPrefixFilterChanged"},
{FlushCacheReason::kEntitlementsTeamIDFilterChanged, @"EntitlementsTeamIDFilterChanged"},
};
for (const auto &kv : reasonToString) {
XCTAssertEqualObjects(FlushCacheReasonToString(kv.first), kv.second);
}
XCTAssertThrows(FlushCacheReasonToString((FlushCacheReason)12345));
XCTAssertThrows(FlushCacheReasonToString(
(FlushCacheReason)(static_cast<int>(FlushCacheReason::kEntitlementsTeamIDFilterChanged) + 1)));
}
@end

View File

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

View File

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

View File

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

View File

@@ -20,9 +20,13 @@
#include <memory>
#include <string>
#import "Source/common/SNTCommonEnums.h"
#include "Source/santad/ProcessTree/process_tree.h"
namespace santa::santad::event_providers::endpoint_security {
class EndpointSecurityAPI;
class MessagePeer;
class Message {
public:
@@ -37,17 +41,36 @@ class Message {
Message(const Message& other);
Message& operator=(const Message& other) = delete;
void SetProcessToken(process_tree::ProcessToken tok);
// Operators to access underlying es_message_t
const es_message_t* operator->() const { return es_msg_; }
const es_message_t& operator*() const { return *es_msg_; }
// Helper to get the API associated with this message.
// Used for things like es_exec_arg_count.
// We should ideally rework this to somehow present these functions as methods
// on the Message, however this would be a bit of a bigger lift.
std::shared_ptr<EndpointSecurityAPI> ESAPI() const { return esapi_; }
std::string ParentProcessName() const;
void UpdateStatState(enum StatChangeStep step) const;
inline StatChangeStep StatChangeStep() const { return stat_change_step_; }
inline StatResult StatResult() const { return stat_result_; }
friend class santa::santad::event_providers::endpoint_security::MessagePeer;
private:
std::shared_ptr<EndpointSecurityAPI> esapi_;
const es_message_t* es_msg_;
std::optional<process_tree::ProcessToken> process_token_;
std::string GetProcessName(pid_t pid) const;
mutable enum StatChangeStep stat_change_step_ = StatChangeStep::kNoChange;
mutable enum StatResult stat_result_ = StatResult::kOK;
};
} // namespace santa::santad::event_providers::endpoint_security

View File

@@ -16,14 +16,16 @@
#include <bsm/libbsm.h>
#include <libproc.h>
#include <sys/stat.h>
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
namespace santa::santad::event_providers::endpoint_security {
Message::Message(std::shared_ptr<EndpointSecurityAPI> esapi, const es_message_t *es_msg)
: esapi_(std::move(esapi)), es_msg_(es_msg) {
: esapi_(std::move(esapi)), es_msg_(es_msg), process_token_(std::nullopt) {
esapi_->RetainMessage(es_msg);
UpdateStatState(StatChangeStep::kMessageCreate);
}
Message::~Message() {
@@ -36,12 +38,42 @@ Message::Message(Message &&other) {
esapi_ = std::move(other.esapi_);
es_msg_ = other.es_msg_;
other.es_msg_ = nullptr;
process_token_ = std::move(other.process_token_);
other.process_token_ = std::nullopt;
stat_change_step_ = other.stat_change_step_;
stat_result_ = other.stat_result_;
}
Message::Message(const Message &other) {
esapi_ = other.esapi_;
es_msg_ = other.es_msg_;
esapi_->RetainMessage(es_msg_);
process_token_ = other.process_token_;
stat_change_step_ = other.stat_change_step_;
stat_result_ = other.stat_result_;
}
void Message::UpdateStatState(enum StatChangeStep step) const {
// Only update state for AUTH EXEC events and if no previous change was detected
if (es_msg_->event_type == ES_EVENT_TYPE_AUTH_EXEC &&
stat_change_step_ == StatChangeStep::kNoChange &&
// Note: The following checks are required due to tests that only
// partially construct an es_message_t.
es_msg_->event.exec.target && es_msg_->event.exec.target->executable) {
struct stat &es_sb = es_msg_->event.exec.target->executable->stat;
struct stat sb;
int ret = stat(es_msg_->event.exec.target->executable->path.data, &sb);
// If stat failed, or if devno/inode changed, update state.
if (ret != 0 || es_sb.st_ino != sb.st_ino || es_sb.st_dev != sb.st_dev) {
stat_change_step_ = step;
// Determine the specific condition that failed for tracking purposes
stat_result_ = (ret != 0) ? StatResult::kStatError : StatResult::kDevnoInodeMismatch;
}
}
}
void Message::SetProcessToken(process_tree::ProcessToken tok) {
process_token_ = std::move(tok);
}
std::string Message::ParentProcessName() const {

View File

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

View File

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

View File

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

View File

@@ -34,6 +34,7 @@
#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h"
#include "Source/santad/Metrics.h"
@@ -50,6 +51,12 @@ class MockAuthResultCache : public AuthResultCache {
MOCK_METHOD(void, FlushCache, (FlushCacheMode mode, FlushCacheReason reason));
};
@interface SNTEndpointSecurityClient (Testing)
@property(nonatomic) double defaultBudget;
@property(nonatomic) int64_t minAllowedHeadroom;
@property(nonatomic) int64_t maxAllowedHeadroom;
@end
@interface SNTEndpointSecurityDeviceManager (Testing)
- (instancetype)init;
- (void)logDiskAppeared:(NSDictionary *)props;
@@ -136,6 +143,11 @@ class MockAuthResultCache : public AuthResultCache {
es_file_t file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&file);
// This test is sensitive to ~1s processing budget.
// Set a 5s headroom and 6s deadline
deviceManager.minAllowedHeadroom = 5 * NSEC_PER_SEC;
deviceManager.maxAllowedHeadroom = 5 * NSEC_PER_SEC;
es_message_t esMsg = MakeESMessage(eventType, &proc, ActionType::Auth, 6000);
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);

View File

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

View File

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

View File

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

View File

@@ -103,7 +103,7 @@ class MockLogger : public Logger {
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
typedef void (^testHelperBlock)(es_message_t *message,
typedef void (^TestHelperBlock)(es_message_t *message,
std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient,
std::shared_ptr<PrefixTree<Unit>> prefixTree,
@@ -112,9 +112,9 @@ typedef void (^testHelperBlock)(es_message_t *message,
es_file_t targetFileMatchesRegex = MakeESFile("/foo/matches");
es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
- (void)handleMessageWithMatchCalls:(BOOL)regexMatchCalls
withMissCalls:(BOOL)regexFailsMatchCalls
withBlock:(testHelperBlock)testBlock {
- (void)handleMessageShouldLog:(BOOL)shouldLog
shouldRemoveFromCache:(BOOL)shouldRemoveFromCache
withBlock:(TestHelperBlock)testBlock {
es_file_t file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&file);
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc, ActionType::Auth);
@@ -127,15 +127,10 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
auto mockEnricher = std::make_shared<MockEnricher>();
if (regexMatchCalls) {
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(std::move(enrichedMsg)));
}
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMatchesRegex)).Times((int)regexMatchCalls);
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMissesRegex))
.Times((int)regexFailsMatchCalls);
if (shouldRemoveFromCache) {
EXPECT_CALL(*mockAuthCache, RemoveFromCache).Times(1);
}
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
// NOTE: Currently unable to create a partial mock of the
@@ -144,7 +139,8 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
// test will mock the `Log` method that is called in the handler block.
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
auto mockLogger = std::make_shared<MockLogger>(nullptr, nullptr);
if (regexMatchCalls) {
if (shouldLog) {
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(std::move(enrichedMsg)));
EXPECT_CALL(*mockLogger, Log).WillOnce(testing::InvokeWithoutArgs(^() {
dispatch_semaphore_signal(sema);
}));
@@ -161,7 +157,8 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
enricher:mockEnricher
compilerController:mockCC
authResultCache:mockAuthCache
prefixTree:prefixTree];
prefixTree:prefixTree
processTree:nullptr];
testBlock(&esMsg, mockESApi, mockCC, recorderClient, prefixTree, &sema, &semaMetrics);
@@ -180,7 +177,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
if (@available(macOS 13.0, *)) {
// CLOSE not modified, but was_mapped_writable, should remove from cache,
// and matches fileChangesRegex
testHelperBlock testBlock =
TestHelperBlock testBlock =
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
__autoreleasing dispatch_semaphore_t *sema,
@@ -212,7 +209,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
if (@available(macOS 13.0, *)) {
// CLOSE not modified, but was_mapped_writable, remove from cache, and does not match
// fileChangesRegex
testHelperBlock testBlock =
TestHelperBlock testBlock =
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
__autoreleasing dispatch_semaphore_t *sema,
@@ -236,7 +233,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
- (void)testHandleMessage {
// CLOSE not modified, bail early
testHelperBlock testBlock = ^(
TestHelperBlock testBlock = ^(
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
@@ -250,7 +247,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
}]);
};
[self handleMessageWithMatchCalls:NO withMissCalls:NO withBlock:testBlock];
[self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock];
// CLOSE modified, remove from cache, and matches fileChangesRegex
testBlock = ^(
@@ -274,7 +271,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
XCTAssertSemaTrue(*sema, 5, "Log wasn't called within expected time window");
};
[self handleMessageWithMatchCalls:YES withMissCalls:NO withBlock:testBlock];
[self handleMessageShouldLog:YES shouldRemoveFromCache:YES withBlock:testBlock];
// CLOSE modified, remove from cache, but doesn't match fileChangesRegex
testBlock = ^(
@@ -285,13 +282,52 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
esMsg->event.close.modified = true;
esMsg->event.close.target = &targetFileMissesRegex;
Message msg(mockESApi, esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
recordEventMetrics:^(EventDisposition d) {
XCTFail("Metrics record callback should not be called here");
}]);
};
[self handleMessageWithMatchCalls:NO withMissCalls:YES withBlock:testBlock];
[self handleMessageShouldLog:NO shouldRemoveFromCache:YES withBlock:testBlock];
// UNLINK, remove from cache, but doesn't match fileChangesRegex
testBlock = ^(
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_UNLINK;
esMsg->event.unlink.target = &targetFileMissesRegex;
Message msg(mockESApi, esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
recordEventMetrics:^(EventDisposition d) {
XCTFail("Metrics record callback should not be called here");
}]);
};
[self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock];
// EXCHANGEDATA, Prefix match, bail early
testBlock = ^(
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_UNLINK;
esMsg->event.exchangedata.file1 = &targetFileMatchesRegex;
prefixTree->InsertPrefix(esMsg->event.exchangedata.file1->path.data, Unit{});
Message msg(mockESApi, esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kDropped);
dispatch_semaphore_signal(*semaMetrics);
}]);
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
};
[self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock];
// LINK, Prefix match, bail early
testBlock =
@@ -316,7 +352,57 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
};
[self handleMessageWithMatchCalls:NO withMissCalls:NO withBlock:testBlock];
[self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock];
// EXIT, EnableForkAndExitLogging is false
testBlock =
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics)
{
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_EXIT;
Message msg(mockESApi, esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
OCMExpect([self.mockConfigurator enableForkAndExitLogging]).andReturn(NO);
[recorderClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kDropped);
dispatch_semaphore_signal(*semaMetrics);
}];
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
};
[self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock];
// FORK, EnableForkAndExitLogging is true
testBlock =
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics)
{
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_FORK;
Message msg(mockESApi, esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
OCMExpect([self.mockConfigurator enableForkAndExitLogging]).andReturn(YES);
[recorderClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kProcessed);
dispatch_semaphore_signal(*semaMetrics);
}];
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
};
[self handleMessageShouldLog:YES shouldRemoveFromCache:NO withBlock:testBlock];
XCTAssertTrue(OCMVerifyAll(self.mockConfigurator));
}
- (void)testGetTargetFileForPrefixTree {
@@ -325,6 +411,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
extern es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg);
es_file_t closeFile = MakeESFile("close");
es_file_t exchangedataFile = MakeESFile("exchangedata");
es_file_t linkFile = MakeESFile("link");
es_file_t renameFile = MakeESFile("rename");
es_file_t unlinkFile = MakeESFile("unlink");
@@ -347,7 +434,8 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &unlinkFile);
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA;
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
esMsg.event.exchangedata.file1 = &exchangedataFile;
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &exchangedataFile);
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC;
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);

View File

@@ -0,0 +1,30 @@
/// Copyright 2024 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/Metrics.h"
#include "Source/santad/ProcessTree/process_tree.h"
@interface SNTEndpointSecurityTreeAwareClient : SNTEndpointSecurityClient
@property std::shared_ptr<santa::santad::process_tree::ProcessTree> processTree;
- (instancetype)
initWithESAPI:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
processor:(santa::santad::Processor)processor
processTree:(std::shared_ptr<santa::santad::process_tree::ProcessTree>)processTree;
@end

View File

@@ -0,0 +1,114 @@
/// Copyright 2024 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/santad/EventProviders/SNTEndpointSecurityTreeAwareClient.h"
#include <EndpointSecurity/EndpointSecurity.h>
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/Metrics.h"
#include "Source/santad/ProcessTree/SNTEndpointSecurityAdapter.h"
#include "Source/santad/ProcessTree/process_tree.h"
#include "Source/santad/ProcessTree/process_tree_macos.h"
using santa::santad::EventDisposition;
using santa::santad::Metrics;
using santa::santad::Processor;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::Message;
@implementation SNTEndpointSecurityTreeAwareClient {
std::vector<bool> _addedEvents;
}
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
metrics:(std::shared_ptr<Metrics>)metrics
processor:(Processor)processor
processTree:
(std::shared_ptr<santa::santad::process_tree::ProcessTree>)processTree {
self = [super initWithESAPI:std::move(esApi) metrics:std::move(metrics) processor:processor];
if (self) {
_processTree = std::move(processTree);
_addedEvents.resize(ES_EVENT_TYPE_LAST, false);
}
return self;
}
// ES guarantees logical consistency within a client (e.g. forks always precede exits),
// however there are no guarantees about the ordering of when messages are delivered _across_
// clients, meaning any client might be the first one to receive process events, and therefore would
// need to be the one to inform the tree. However not all clients are interested in or subscribe to
// process events. This (and the below handleContextMessage) ensures that the ES subscription for
// all clients includes the minimal required set of events for process tree (NOTIFY_FORK, some EXEC
// variant, and NOTIFY_EXIT) but also filters out any events that were subscribed to solely for the
// purpose of updating the tree from being processed downstream, where they would be unexpected.
- (bool)subscribe:(const std::set<es_event_type_t> &)events {
std::set<es_event_type_t> eventsWithLifecycle = events;
if (events.find(ES_EVENT_TYPE_NOTIFY_FORK) == events.end()) {
eventsWithLifecycle.insert(ES_EVENT_TYPE_NOTIFY_FORK);
_addedEvents[ES_EVENT_TYPE_NOTIFY_FORK] = true;
}
if (events.find(ES_EVENT_TYPE_NOTIFY_EXEC) == events.end() &&
events.find(ES_EVENT_TYPE_AUTH_EXEC) == events.end()) {
eventsWithLifecycle.insert(ES_EVENT_TYPE_NOTIFY_EXEC);
_addedEvents[ES_EVENT_TYPE_NOTIFY_EXEC] = true;
}
if (events.find(ES_EVENT_TYPE_NOTIFY_EXIT) == events.end()) {
eventsWithLifecycle.insert(ES_EVENT_TYPE_NOTIFY_EXIT);
_addedEvents[ES_EVENT_TYPE_NOTIFY_EXIT] = true;
}
return [super subscribe:eventsWithLifecycle];
}
- (bool)handleContextMessage:(Message &)esMsg {
if (!_processTree) {
return false;
}
// Inform the tree
switch (esMsg->event_type) {
case ES_EVENT_TYPE_NOTIFY_FORK:
case ES_EVENT_TYPE_NOTIFY_EXEC:
case ES_EVENT_TYPE_AUTH_EXEC:
case ES_EVENT_TYPE_NOTIFY_EXIT:
santa::santad::process_tree::InformFromESEvent(*_processTree, esMsg);
break;
default: break;
}
// Now enumerate the processes that processing this event might require access to...
std::vector<struct santa::santad::process_tree::Pid> pids;
pids.emplace_back(santa::santad::process_tree::PidFromAuditToken(esMsg->process->audit_token));
switch (esMsg->event_type) {
case ES_EVENT_TYPE_AUTH_EXEC:
case ES_EVENT_TYPE_NOTIFY_EXEC:
pids.emplace_back(
santa::santad::process_tree::PidFromAuditToken(esMsg->event.exec.target->audit_token));
break;
case ES_EVENT_TYPE_NOTIFY_FORK:
pids.emplace_back(
santa::santad::process_tree::PidFromAuditToken(esMsg->event.fork.child->audit_token));
break;
default: break;
}
// ...and create the token for those.
esMsg.SetProcessToken(santa::santad::process_tree::ProcessToken(_processTree, std::move(pids)));
return _addedEvents[esMsg->event_type];
}
@end

View File

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

View File

@@ -23,7 +23,7 @@
#include <string_view>
#include <vector>
#include "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTCommonEnums.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
@@ -143,11 +143,11 @@ class MockWriter : public Null {
mockESApi->SetExpectationsRetainReleaseMessage();
{
auto enrichedMsg = std::make_unique<EnrichedMessage>(
EnrichedClose(Message(mockESApi, &msg),
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
auto enrichedMsg = std::make_unique<EnrichedMessage>(EnrichedClose(
Message(mockESApi, &msg),
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
EnrichedFile(std::nullopt, std::nullopt, std::nullopt), std::nullopt),
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
EXPECT_CALL(*mockSerializer, SerializeMessage(testing::A<const EnrichedClose &>())).Times(1);
EXPECT_CALL(*mockWriter, Write).Times(1);
@@ -229,10 +229,11 @@ class MockWriter : public Null {
EXPECT_CALL(*mockWriter, Write);
Logger(mockSerializer, mockWriter)
.LogFileAccess("v1", "name", Message(mockESApi, &msg),
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
"tgt", FileAccessPolicyDecision::kDenied);
.LogFileAccess(
"v1", "name", Message(mockESApi, &msg),
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
EnrichedFile(std::nullopt, std::nullopt, std::nullopt), std::nullopt),
"tgt", FileAccessPolicyDecision::kDenied);
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());

View File

@@ -94,12 +94,14 @@ std::string GetReasonString(SNTEventState event_state) {
case SNTEventStateAllowScope: return "SCOPE";
case SNTEventStateAllowTeamID: return "TEAMID";
case SNTEventStateAllowSigningID: return "SIGNINGID";
case SNTEventStateAllowCDHash: return "CDHASH";
case SNTEventStateAllowUnknown: return "UNKNOWN";
case SNTEventStateBlockBinary: return "BINARY";
case SNTEventStateBlockCertificate: return "CERT";
case SNTEventStateBlockScope: return "SCOPE";
case SNTEventStateBlockTeamID: return "TEAMID";
case SNTEventStateBlockSigningID: return "SIGNINGID";
case SNTEventStateBlockCDHash: return "CDHASH";
case SNTEventStateBlockLongPath: return "LONG_PATH";
case SNTEventStateBlockUnknown: return "UNKNOWN";
default: return "NOTRUNNING";

View File

@@ -436,6 +436,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
{SNTEventStateBlockScope, "SCOPE"},
{SNTEventStateBlockTeamID, "TEAMID"},
{SNTEventStateBlockSigningID, "SIGNINGID"},
{SNTEventStateBlockCDHash, "CDHASH"},
{SNTEventStateBlockLongPath, "LONG_PATH"},
{SNTEventStateAllowUnknown, "UNKNOWN"},
{SNTEventStateAllowBinary, "BINARY"},
@@ -446,6 +447,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
{SNTEventStateAllowPendingTransitive, "PENDING_TRANSITIVE"},
{SNTEventStateAllowTeamID, "TEAMID"},
{SNTEventStateAllowSigningID, "SIGNINGID"},
{SNTEventStateAllowCDHash, "CDHASH"},
};
for (const auto &kv : stateToReason) {

View File

@@ -71,6 +71,9 @@ namespace pbv1 = ::santa::pb::v1;
namespace santa::santad::logs::endpoint_security::serializers {
static constexpr NSUInteger kMaxEncodeObjectEntries = 64;
static constexpr NSUInteger kMaxEncodeObjectLevels = 5;
std::shared_ptr<Protobuf> Protobuf::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
SNTDecisionCache *decision_cache, bool json) {
return std::make_shared<Protobuf>(esapi, std::move(decision_cache), json);
@@ -182,6 +185,14 @@ static inline void EncodeFileInfoLight(::pbv1::FileInfoLight *pb_file, const es_
pb_file->set_truncated(es_file->path_truncated);
}
static inline void EncodeAnnotations(std::function<::pbv1::process_tree::Annotations *()> lazy_f,
const EnrichedProcess &enriched_proc) {
if (std::optional<pbv1::process_tree::Annotations> proc_annotations = enriched_proc.annotations();
proc_annotations) {
*lazy_f() = *proc_annotations;
}
}
static inline void EncodeProcessInfoLight(::pbv1::ProcessInfoLight *pb_proc_info,
uint32_t message_version, const es_process_t *es_proc,
const EnrichedProcess &enriched_proc) {
@@ -202,6 +213,8 @@ static inline void EncodeProcessInfoLight(::pbv1::ProcessInfoLight *pb_proc_info
enriched_proc.real_group());
EncodeFileInfoLight(pb_proc_info->mutable_executable(), es_proc->executable);
EncodeAnnotations([pb_proc_info] { return pb_proc_info->mutable_annotations(); }, enriched_proc);
}
static inline void EncodeProcessInfo(::pbv1::ProcessInfo *pb_proc_info, uint32_t message_version,
@@ -253,6 +266,8 @@ static inline void EncodeProcessInfo(::pbv1::ProcessInfo *pb_proc_info, uint32_t
if (message_version >= 3) {
EncodeTimestamp(pb_proc_info->mutable_start_time(), es_proc->start_time);
}
EncodeAnnotations([pb_proc_info] { return pb_proc_info->mutable_annotations(); }, enriched_proc);
}
void EncodeExitStatus(::pbv1::Exit *pb_exit, int exitStatus) {
@@ -296,12 +311,14 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
case SNTEventStateAllowScope: return ::pbv1::Execution::REASON_SCOPE;
case SNTEventStateAllowTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
case SNTEventStateAllowSigningID: return ::pbv1::Execution::REASON_SIGNING_ID;
case SNTEventStateAllowCDHash: return ::pbv1::Execution::REASON_CDHASH;
case SNTEventStateAllowUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
case SNTEventStateBlockBinary: return ::pbv1::Execution::REASON_BINARY;
case SNTEventStateBlockCertificate: return ::pbv1::Execution::REASON_CERT;
case SNTEventStateBlockScope: return ::pbv1::Execution::REASON_SCOPE;
case SNTEventStateBlockTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
case SNTEventStateBlockSigningID: return ::pbv1::Execution::REASON_SIGNING_ID;
case SNTEventStateBlockCDHash: return ::pbv1::Execution::REASON_CDHASH;
case SNTEventStateBlockLongPath: return ::pbv1::Execution::REASON_LONG_PATH;
case SNTEventStateBlockUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
default: return ::pbv1::Execution::REASON_NOT_RUNNING;
@@ -364,7 +381,7 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
::pbv1::SantaMessage *Protobuf::CreateDefaultProto(Arena *arena, struct timespec event_time,
struct timespec processed_time) {
::pbv1::SantaMessage *santa_msg = Arena::CreateMessage<::pbv1::SantaMessage>(arena);
::pbv1::SantaMessage *santa_msg = Arena::Create<::pbv1::SantaMessage>(arena);
if (EnabledMachineID()) {
EncodeString([santa_msg] { return santa_msg->mutable_machine_id(); }, MachineID());
@@ -398,7 +415,7 @@ std::vector<uint8_t> Protobuf::FinalizeProto(::pbv1::SantaMessage *santa_msg) {
// TODO: Profile this. It's probably not the most efficient way to do this.
JsonPrintOptions options;
options.always_print_enums_as_ints = false;
options.always_print_primitive_fields = true;
options.always_print_fields_with_no_presence = true;
options.preserve_proto_field_names = true;
std::string json;
@@ -449,6 +466,124 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExchange &msg) {
return FinalizeProto(santa_msg);
}
id StandardizedNestedObjects(id obj, int level) {
if (level-- == 0) {
return [obj description];
}
if ([obj isKindOfClass:[NSNumber class]] || [obj isKindOfClass:[NSString class]]) {
return obj;
} else if ([obj isKindOfClass:[NSArray class]]) {
NSMutableArray *arr = [NSMutableArray array];
for (id item in obj) {
[arr addObject:StandardizedNestedObjects(item, level)];
}
return arr;
} else if ([obj isKindOfClass:[NSDictionary class]]) {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (id key in obj) {
[dict setObject:StandardizedNestedObjects(obj[key], level) forKey:key];
}
return dict;
} else if ([obj isKindOfClass:[NSData class]]) {
return [obj base64EncodedStringWithOptions:0];
} else if ([obj isKindOfClass:[NSDate class]]) {
return [NSISO8601DateFormatter stringFromDate:obj
timeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]
formatOptions:NSISO8601DateFormatWithFractionalSeconds |
NSISO8601DateFormatWithInternetDateTime];
} else {
LOGW(@"Unexpected object encountered: %@", obj);
return [obj description];
}
}
void EncodeEntitlements(::pbv1::Execution *pb_exec, SNTCachedDecision *cd) {
::pbv1::EntitlementInfo *pb_entitlement_info = pb_exec->mutable_entitlement_info();
pb_entitlement_info->set_entitlements_filtered(cd.entitlementsFiltered != NO);
if (!cd.entitlements) {
return;
}
// Since nested objects with varying types is hard for the API to serialize to
// JSON, first go through and standardize types to ensure better serialization
// as well as a consitent view of data.
NSDictionary *entitlements = StandardizedNestedObjects(cd.entitlements, kMaxEncodeObjectLevels);
__block int numObjectsToEncode = (int)std::min(kMaxEncodeObjectEntries, entitlements.count);
pb_entitlement_info->mutable_entitlements()->Reserve(numObjectsToEncode);
[entitlements enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if (numObjectsToEncode-- == 0) {
// Because entitlements are being clipped, ensure that we update that
// the set of entitlements were filtered.
pb_entitlement_info->set_entitlements_filtered(true);
*stop = YES;
return;
}
if (![key isKindOfClass:[NSString class]]) {
LOGW(@"Skipping entitlement key with unexpected key type: %@", key);
return;
}
NSError *err;
NSData *jsonData;
@try {
jsonData = [NSJSONSerialization dataWithJSONObject:obj
options:NSJSONWritingFragmentsAllowed
error:&err];
} @catch (NSException *e) {
LOGW(@"Encountered entitlement that cannot directly convert to JSON: %@: %@", key, obj);
}
if (!jsonData) {
// If the first attempt to serialize to JSON failed, get a string
// representation of the object via the `description` method and attempt
// to serialize that instead. Serialization can fail for a number of
// reasons, such as arrays including invalid types.
@try {
jsonData = [NSJSONSerialization dataWithJSONObject:[obj description]
options:NSJSONWritingFragmentsAllowed
error:&err];
} @catch (NSException *e) {
LOGW(@"Unable to create fallback string: %@: %@", key, obj);
}
if (!jsonData) {
@try {
// As a final fallback, simply serialize an error message so that the
// entitlement key is still logged.
jsonData = [NSJSONSerialization dataWithJSONObject:@"JSON Serialization Failed"
options:NSJSONWritingFragmentsAllowed
error:&err];
} @catch (NSException *e) {
// This shouldn't be able to happen...
LOGW(@"Failed to serialize fallback error message");
}
}
}
// This shouldn't be possible given the fallback code above. But handle it
// just in case to prevent a crash.
if (!jsonData) {
LOGW(@"Failed to create valid JSON for entitlement: %@", key);
return;
}
::pbv1::Entitlement *pb_entitlement = pb_entitlement_info->add_entitlements();
EncodeString([pb_entitlement] { return pb_entitlement->mutable_key(); },
NSStringToUTF8StringView(key));
EncodeString([pb_entitlement] { return pb_entitlement->mutable_value(); },
NSStringToUTF8StringView([[NSString alloc] initWithData:jsonData
encoding:NSUTF8StringEncoding]));
}];
}
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCachedDecision *cd) {
Arena arena;
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
@@ -525,6 +660,8 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCach
NSString *orig_path = Utilities::OriginalPathForTranslocation(msg.es_msg().event.exec.target);
EncodeString([pb_exec] { return pb_exec->mutable_original_path(); }, orig_path);
EncodeEntitlements(pb_exec, cd);
return FinalizeProto(santa_msg);
}

View File

@@ -28,7 +28,7 @@
#include <cstring>
#import "Source/common/SNTCachedDecision.h"
#include "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTStoredEvent.h"
#include "Source/common/TestUtils.h"
@@ -60,6 +60,7 @@ namespace pbv1 = ::santa::pb::v1;
namespace santa::santad::logs::endpoint_security::serializers {
extern void EncodeExitStatus(::pbv1::Exit *pbExit, int exitStatus);
extern void EncodeEntitlements(::pbv1::Execution *pb_exec, SNTCachedDecision *cd);
extern ::pbv1::Execution::Decision GetDecisionEnum(SNTEventState event_state);
extern ::pbv1::Execution::Reason GetReasonEnum(SNTEventState event_state);
extern ::pbv1::Execution::Mode GetModeEnum(SNTClientMode mode);
@@ -68,6 +69,7 @@ extern ::pbv1::FileAccess::AccessType GetAccessType(es_event_type_t event_type);
extern ::pbv1::FileAccess::PolicyDecision GetPolicyDecision(FileAccessPolicyDecision decision);
} // namespace santa::santad::logs::endpoint_security::serializers
using santa::santad::logs::endpoint_security::serializers::EncodeEntitlements;
using santa::santad::logs::endpoint_security::serializers::EncodeExitStatus;
using santa::santad::logs::endpoint_security::serializers::GetAccessType;
using santa::santad::logs::endpoint_security::serializers::GetDecisionEnum;
@@ -76,30 +78,21 @@ using santa::santad::logs::endpoint_security::serializers::GetModeEnum;
using santa::santad::logs::endpoint_security::serializers::GetPolicyDecision;
using santa::santad::logs::endpoint_security::serializers::GetReasonEnum;
@interface ProtobufTest : XCTestCase
@property id mockConfigurator;
@property id mockDecisionCache;
@property SNTCachedDecision *testCachedDecision;
@end
JsonPrintOptions DefaultJsonPrintOptions() {
JsonPrintOptions options;
options.always_print_enums_as_ints = false;
options.always_print_primitive_fields = false;
options.always_print_fields_with_no_presence = false;
options.preserve_proto_field_names = true;
options.add_whitespace = true;
return options;
}
NSString *TestJsonPath(NSString *jsonFileName, uint32_t version) {
static dispatch_once_t onceToken;
static NSString *testPath;
static NSString *testDataRepoPath = @"santa/Source/santad/testdata/protobuf";
NSString *testDataRepoVersionPath = [NSString stringWithFormat:@"v%u", version];
dispatch_once(&onceToken, ^{
testPath = [NSString pathWithComponents:@[
[[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testDataRepoPath
]];
});
return [NSString pathWithComponents:@[ testPath, testDataRepoVersionPath, jsonFileName ]];
}
NSString *EventTypeToFilename(es_event_type_t eventType) {
switch (eventType) {
case ES_EVENT_TYPE_NOTIFY_CLOSE: return @"close.json";
@@ -115,6 +108,16 @@ NSString *EventTypeToFilename(es_event_type_t eventType) {
}
}
NSString *TestJsonPath(NSString *jsonFileName, uint32_t version) {
NSString *p = [NSString pathWithComponents:@[
[[NSBundle bundleForClass:[ProtobufTest class]] resourcePath],
@"protobuf",
[NSString stringWithFormat:@"v%u", version],
jsonFileName,
]];
return p;
}
NSString *LoadTestJson(NSString *jsonFileName, uint32_t version) {
NSError *err = nil;
NSString *jsonData = [NSString stringWithContentsOfFile:TestJsonPath(jsonFileName, version)
@@ -166,28 +169,35 @@ std::string ConvertMessageToJsonString(const ::pbv1::SantaMessage &santaMsg) {
return json;
}
NSDictionary *findDelta(NSDictionary *a, NSDictionary *b) {
NSMutableDictionary *delta = NSMutableDictionary.dictionary;
NSDictionary *FindDelta(NSDictionary *want, NSDictionary *got) {
NSMutableDictionary *delta = [NSMutableDictionary dictionary];
delta[@"want"] = [NSMutableDictionary dictionary];
delta[@"got"] = [NSMutableDictionary dictionary];
// Find objects in a that don't exist or are different in b.
[a enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) {
id otherObj = b[key];
// Find objects in `want` that don't exist or are different in `got`.
[want enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
id otherObj = got[key];
if (![obj isEqual:otherObj]) {
delta[key] = obj;
if (!otherObj) {
delta[@"want"][key] = obj;
delta[@"got"][key] = @"Key missing";
} else if (![obj isEqual:otherObj]) {
delta[@"want"][key] = obj;
delta[@"got"][key] = otherObj;
}
}];
// Find objects in the other dictionary that don't exist in self
[b enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) {
id aObj = a[key];
// Find objects in `got` that don't exist in `want`
[got enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
id aObj = want[key];
if (!aObj) {
delta[key] = obj;
delta[@"want"][key] = @"Key missing";
delta[@"got"][key] = obj;
}
}];
return delta;
return [delta[@"want"] count] > 0 ? delta : nil;
}
void SerializeAndCheck(es_event_type_t eventType,
@@ -268,9 +278,9 @@ void SerializeAndCheck(es_event_type_t eventType,
error:&jsonError];
XCTAssertNil(jsonError, @"failed to parse got data as JSON");
XCTAssertNil(FindDelta(wantJSONDict, gotJSONDict));
// Note: Uncomment this line to help create testfile JSON when the assert above fails
// XCTAssertEqualObjects([NSString stringWithUTF8String:gotData.c_str()], wantData);
NSDictionary *delta = findDelta(wantJSONDict, gotJSONDict);
XCTAssertEqualObjects(@{}, delta);
}
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
@@ -316,12 +326,6 @@ void SerializeAndCheckNonESEvents(
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
@interface ProtobufTest : XCTestCase
@property id mockConfigurator;
@property id mockDecisionCache;
@property SNTCachedDecision *testCachedDecision;
@end
@implementation ProtobufTest
- (void)setUp {
@@ -338,6 +342,22 @@ void SerializeAndCheckNonESEvents(
self.testCachedDecision.quarantineURL = @"google.com";
self.testCachedDecision.certSHA256 = @"5678_cert_hash";
self.testCachedDecision.decisionClientMode = SNTClientModeLockdown;
self.testCachedDecision.entitlements = @{
@"key_with_str_val" : @"bar",
@"key_with_num_val" : @(1234),
@"key_with_date_val" : [NSDate dateWithTimeIntervalSince1970:1699376402],
@"key_with_data_val" : [@"Hello World" dataUsingEncoding:NSUTF8StringEncoding],
@"key_with_arr_val" : @[ @"v1", @"v2", @"v3" ],
@"key_with_arr_val_nested" : @[ @"v1", @"v2", @"v3", @[ @"nv1", @"nv2" ] ],
@"key_with_arr_val_multitype" :
@[ @"v1", @"v2", @"v3", @(123), [NSDate dateWithTimeIntervalSince1970:1699376402] ],
@"key_with_dict_val" : @{@"k1" : @"v1", @"k2" : @"v2"},
@"key_with_dict_val_nested" : @{
@"k1" : @"v1",
@"k2" : @"v2",
@"k3" : @{@"nk1" : @"nv1", @"nk2" : [NSDate dateWithTimeIntervalSince1970:1699376402]}
},
};
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
@@ -418,6 +438,7 @@ void SerializeAndCheckNonESEvents(
{SNTEventStateBlockScope, ::pbv1::Execution::REASON_SCOPE},
{SNTEventStateBlockTeamID, ::pbv1::Execution::REASON_TEAM_ID},
{SNTEventStateBlockSigningID, ::pbv1::Execution::REASON_SIGNING_ID},
{SNTEventStateBlockCDHash, ::pbv1::Execution::REASON_CDHASH},
{SNTEventStateBlockLongPath, ::pbv1::Execution::REASON_LONG_PATH},
{SNTEventStateAllowUnknown, ::pbv1::Execution::REASON_UNKNOWN},
{SNTEventStateAllowBinary, ::pbv1::Execution::REASON_BINARY},
@@ -428,6 +449,7 @@ void SerializeAndCheckNonESEvents(
{SNTEventStateAllowPendingTransitive, ::pbv1::Execution::REASON_PENDING_TRANSITIVE},
{SNTEventStateAllowTeamID, ::pbv1::Execution::REASON_TEAM_ID},
{SNTEventStateAllowSigningID, ::pbv1::Execution::REASON_SIGNING_ID},
{SNTEventStateAllowCDHash, ::pbv1::Execution::REASON_CDHASH},
};
for (const auto &kv : stateToReason) {
@@ -573,6 +595,70 @@ void SerializeAndCheckNonESEvents(
json:YES];
}
- (void)testEncodeEntitlements {
int kMaxEncodeObjectEntries = 64; // From Protobuf.mm
// Test basic encoding without filtered entitlements
{
::pbv1::Execution pbExec;
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
cd.entitlements = @{@"com.google.test" : @(YES)};
XCTAssertEqual(0, pbExec.entitlement_info().entitlements_size());
XCTAssertFalse(cd.entitlementsFiltered);
XCTAssertEqual(1, cd.entitlements.count);
EncodeEntitlements(&pbExec, cd);
XCTAssertEqual(1, pbExec.entitlement_info().entitlements_size());
XCTAssertTrue(pbExec.entitlement_info().has_entitlements_filtered());
XCTAssertFalse(pbExec.entitlement_info().entitlements_filtered());
}
// Test basic encoding with filtered entitlements
{
::pbv1::Execution pbExec;
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
cd.entitlements = @{@"com.google.test" : @(YES), @"com.google.test2" : @(NO)};
cd.entitlementsFiltered = YES;
XCTAssertEqual(0, pbExec.entitlement_info().entitlements_size());
XCTAssertTrue(cd.entitlementsFiltered);
XCTAssertEqual(2, cd.entitlements.count);
EncodeEntitlements(&pbExec, cd);
XCTAssertEqual(2, pbExec.entitlement_info().entitlements_size());
XCTAssertTrue(pbExec.entitlement_info().has_entitlements_filtered());
XCTAssertTrue(pbExec.entitlement_info().entitlements_filtered());
}
// Test max number of entitlements logged
// When entitlements are clipped, `entitlements_filtered` is set to true
{
::pbv1::Execution pbExec;
NSMutableDictionary *ents = [NSMutableDictionary dictionary];
for (int i = 0; i < 100; i++) {
ents[[NSString stringWithFormat:@"k%d", i]] = @(i);
}
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
cd.entitlements = ents;
XCTAssertEqual(0, pbExec.entitlement_info().entitlements_size());
XCTAssertFalse(cd.entitlementsFiltered);
XCTAssertGreaterThan(cd.entitlements.count, kMaxEncodeObjectEntries);
EncodeEntitlements(&pbExec, cd);
XCTAssertEqual(kMaxEncodeObjectEntries, pbExec.entitlement_info().entitlements_size());
XCTAssertTrue(pbExec.entitlement_info().has_entitlements_filtered());
XCTAssertTrue(pbExec.entitlement_info().entitlements_filtered());
}
}
- (void)testSerializeMessageExit {
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_EXIT
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,

View File

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

View File

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

View File

@@ -23,6 +23,7 @@
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool_platform_specific.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
namespace fsspool {

View File

@@ -26,6 +26,7 @@
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTMetricSet.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
namespace santa::santad {
@@ -53,6 +54,8 @@ enum class FileAccessMetricStatus {
using EventCountTuple = std::tuple<Processor, es_event_type_t, EventDisposition>;
using EventTimesTuple = std::tuple<Processor, es_event_type_t>;
using EventStatsTuple = std::tuple<Processor, es_event_type_t>;
using EventStatChangeTuple = std::tuple<StatChangeStep, StatResult>;
using FileAccessMetricsPolicyVersion = std::string;
using FileAccessMetricsPolicyName = std::string;
using FileAccessEventCountTuple =
@@ -65,7 +68,8 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *faa_event_counts,
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *drop_counts,
SNTMetricCounter *faa_event_counts, SNTMetricCounter *stat_change_counts,
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *));
~Metrics();
@@ -78,8 +82,11 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
// Force an immediate flush and export of metrics
void Export();
void SetEventMetrics(Processor processor, es_event_type_t event_type,
EventDisposition disposition, int64_t nanos);
// Used for tracking event sequence numbers to determine if drops occured
void UpdateEventStats(Processor processor, const es_message_t *msg);
void SetEventMetrics(Processor processor, EventDisposition event_disposition, int64_t nanos,
const santa::santad::event_providers::endpoint_security::Message &msg);
void SetRateLimitingMetrics(Processor processor, int64_t events_rate_limited_count);
@@ -90,6 +97,11 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
friend class santa::santad::MetricsPeer;
private:
struct SequenceStats {
uint64_t next_seq_num = 0;
int64_t drops = 0;
};
void FlushMetrics();
void ExportLocked(SNTMetricSet *metric_set);
@@ -101,6 +113,8 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
SNTMetricCounter *event_counts_;
SNTMetricCounter *rate_limit_counts_;
SNTMetricCounter *faa_event_counts_;
SNTMetricCounter *drop_counts_;
SNTMetricCounter *stat_change_counts_;
SNTMetricSet *metric_set_;
// Tracks whether or not the timer_source should be running.
// This helps manage dispatch source state to ensure the source is not
@@ -117,6 +131,8 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
std::map<EventTimesTuple, int64_t> event_times_cache_;
std::map<Processor, int64_t> rate_limit_counts_cache_;
std::map<FileAccessEventCountTuple, int64_t> faa_event_counts_cache_;
std::map<EventStatsTuple, SequenceStats> drop_cache_;
std::map<EventStatChangeTuple, int64_t> stat_change_cache_;
};
} // namespace santa::santad

View File

@@ -13,6 +13,7 @@
/// limitations under the License.
#include "Source/santad/Metrics.h"
#include <EndpointSecurity/ESTypes.h>
#include <memory>
@@ -49,10 +50,19 @@ static NSString *const kEventTypeNotifyLink = @"NotifyLink";
static NSString *const kEventTypeNotifyRename = @"NotifyRename";
static NSString *const kEventTypeNotifyUnlink = @"NotifyUnlink";
static NSString *const kEventTypeNotifyUnmount = @"NotifyUnmount";
static NSString *const kPseudoEventTypeGlobal = @"Global";
static NSString *const kEventDispositionDropped = @"Dropped";
static NSString *const kEventDispositionProcessed = @"Processed";
static NSString *const kStatChangeStepNoChange = @"NoChange";
static NSString *const kStatChangeStepMessageCreate = @"MessageCreate";
static NSString *const kStatChangeStepCodesignValidation = @"CodesignValidation";
static NSString *const kStatResultOK = @"OK";
static NSString *const kStatResultStatError = @"StatError";
static NSString *const kStatResultDevnoInodeMismatch = @"DevnoInodeMismatch";
// Compat values
static NSString *const kFileAccessMetricStatusOK = @"OK";
static NSString *const kFileAccessMetricStatusBlockedUser = @"BLOCKED_USER";
@@ -103,6 +113,7 @@ NSString *const EventTypeToString(es_event_type_t eventType) {
case ES_EVENT_TYPE_NOTIFY_RENAME: return kEventTypeNotifyRename;
case ES_EVENT_TYPE_NOTIFY_UNLINK: return kEventTypeNotifyUnlink;
case ES_EVENT_TYPE_NOTIFY_UNMOUNT: return kEventTypeNotifyUnmount;
case ES_EVENT_TYPE_LAST: return kPseudoEventTypeGlobal;
default:
[NSException raise:@"Invalid event type" format:@"Invalid event type: %d", eventType];
return nil;
@@ -146,6 +157,30 @@ NSString *const FileAccessPolicyDecisionToString(FileAccessPolicyDecision decisi
}
}
NSString *const StatChangeStepToString(StatChangeStep step) {
switch (step) {
case StatChangeStep::kNoChange: return kStatChangeStepNoChange;
case StatChangeStep::kMessageCreate: return kStatChangeStepMessageCreate;
case StatChangeStep::kCodesignValidation: return kStatChangeStepCodesignValidation;
default:
[NSException raise:@"Invalid stat change step"
format:@"Unknown stat change step value: %d", static_cast<int>(step)];
return nil;
}
}
NSString *const StatResultToString(StatResult result) {
switch (result) {
case StatResult::kOK: return kStatResultOK;
case StatResult::kStatError: return kStatResultStatError;
case StatResult::kDevnoInodeMismatch: return kStatResultDevnoInodeMismatch;
default:
[NSException raise:@"Invalid stat result"
format:@"Unknown stat result value: %d", static_cast<int>(result)];
return nil;
}
}
std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t interval) {
dispatch_queue_t q = dispatch_queue_create("com.google.santa.santametricsservice.q",
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
@@ -174,12 +209,22 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
]
helpText:@"Count of times a log is emitted from the File Access Authorizer client"];
std::shared_ptr<Metrics> metrics =
std::make_shared<Metrics>(q, timer_source, interval, event_processing_times, event_counts,
rate_limit_counts, faa_event_counts, metric_set, ^(Metrics *metrics) {
SNTRegisterCoreMetrics();
metrics->EstablishConnection();
});
SNTMetricCounter *drop_counts =
[metric_set counterWithName:@"/santa/event_drop_count"
fieldNames:@[ @"Processor", @"Event" ]
helpText:@"Count of the number of drops for each event"];
SNTMetricCounter *stat_change_counts =
[metric_set counterWithName:@"/santa/event_stat_change_count"
fieldNames:@[ @"step", @"error" ]
helpText:@"Count of times a stat info changed for a binary being evalauted"];
std::shared_ptr<Metrics> metrics = std::make_shared<Metrics>(
q, timer_source, interval, event_processing_times, event_counts, rate_limit_counts,
faa_event_counts, drop_counts, stat_change_counts, metric_set, ^(Metrics *metrics) {
SNTRegisterCoreMetrics();
metrics->EstablishConnection();
});
std::weak_ptr<Metrics> weak_metrics(metrics);
dispatch_source_set_event_handler(metrics->timer_source_, ^{
@@ -197,6 +242,7 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
Metrics::Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *faa_event_counts,
SNTMetricCounter *drop_counts, SNTMetricCounter *stat_change_counts,
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *))
: q_(q),
timer_source_(timer_source),
@@ -205,6 +251,8 @@ Metrics::Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t in
event_counts_(event_counts),
rate_limit_counts_(rate_limit_counts),
faa_event_counts_(faa_event_counts),
drop_counts_(drop_counts),
stat_change_counts_(stat_change_counts),
metric_set_(metric_set),
run_on_first_start_(run_on_first_start) {
SetInterval(interval_);
@@ -285,11 +333,36 @@ void Metrics::FlushMetrics() {
]];
}
for (auto &[key, stats] : drop_cache_) {
if (stats.drops > 0) {
NSString *processorName = ProcessorToString(std::get<Processor>(key));
NSString *eventName = EventTypeToString(std::get<es_event_type_t>(key));
[drop_counts_ incrementBy:stats.drops forFieldValues:@[ processorName, eventName ]];
// Reset drops to 0, but leave sequence number intact. Sequence numbers
// must persist so that accurate drops can be counted.
stats.drops = 0;
}
}
for (const auto &[key, count] : stat_change_cache_) {
if (count > 0) {
NSString *stepName = StatChangeStepToString(std::get<StatChangeStep>(key));
NSString *error = StatResultToString(std::get<StatResult>(key));
[stat_change_counts_ incrementBy:count forFieldValues:@[ stepName, error ]];
}
}
// Reset the maps so the next cycle begins with a clean state
// IMPORTANT: Do not reset drop_cache_, the sequence numbers must persist
// for accurate accounting
event_counts_cache_ = {};
event_times_cache_ = {};
rate_limit_counts_cache_ = {};
faa_event_counts_cache_ = {};
stat_change_cache_ = {};
});
}
@@ -331,11 +404,52 @@ void Metrics::StopPoll() {
});
}
void Metrics::SetEventMetrics(Processor processor, es_event_type_t event_type,
EventDisposition event_disposition, int64_t nanos) {
void Metrics::SetEventMetrics(
Processor processor, EventDisposition event_disposition, int64_t nanos,
const santa::santad::event_providers::endpoint_security::Message &msg) {
dispatch_sync(events_q_, ^{
event_counts_cache_[EventCountTuple{processor, event_type, event_disposition}]++;
event_times_cache_[EventTimesTuple{processor, event_type}] = nanos;
event_counts_cache_[EventCountTuple{processor, msg->event_type, event_disposition}]++;
event_times_cache_[EventTimesTuple{processor, msg->event_type}] = nanos;
// Stat changes are only tracked for AUTH EXEC events
if (msg->event_type == ES_EVENT_TYPE_AUTH_EXEC) {
stat_change_cache_[EventStatChangeTuple{msg.StatChangeStep(), msg.StatResult()}]++;
}
});
}
void Metrics::UpdateEventStats(Processor processor, const es_message_t *msg) {
dispatch_sync(events_q_, ^{
EventStatsTuple event_stats_key{processor, msg->event_type};
// NB: Using the invalid event type ES_EVENT_TYPE_LAST to store drop counts
// based on the global sequence number.
EventStatsTuple global_stats_key{processor, ES_EVENT_TYPE_LAST};
SequenceStats old_event_stats = drop_cache_[event_stats_key];
SequenceStats old_global_stats = drop_cache_[global_stats_key];
// Sequence number should always increment by 1.
// Drops detected if there is a difference between the current sequence
// number and the expected sequence number
int64_t new_event_drops = msg->seq_num - old_event_stats.next_seq_num;
int64_t new_global_drops = msg->global_seq_num - old_global_stats.next_seq_num;
// Only log one or the other, prefer the event specific log.
// For higher volume event types, we'll normally see the event-specific log eventually
// For lower volume event types, we may only see the global drop message
if (new_event_drops > 0) {
LOGD(@"Drops detected for client: %@, event: %@, drops: %llu", ProcessorToString(processor),
EventTypeToString(msg->event_type), new_event_drops);
} else if (new_global_drops > 0) {
LOGD(@"Drops detected globally for client: %@, drops: %llu", ProcessorToString(processor),
new_global_drops);
}
drop_cache_[event_stats_key] = SequenceStats{.next_seq_num = msg->seq_num + 1,
.drops = old_event_stats.drops + new_event_drops};
drop_cache_[global_stats_key] = SequenceStats{
.next_seq_num = msg->global_seq_num + 1, .drops = old_global_stats.drops + new_global_drops};
});
}

View File

@@ -12,6 +12,8 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include "Source/santad/Metrics.h"
#include <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
@@ -19,13 +21,18 @@
#include <dispatch/dispatch.h>
#include <map>
#include <memory>
#import "Source/common/SNTCommonEnums.h"
#include "Source/common/SNTMetricSet.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/Metrics.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
using santa::santad::EventCountTuple;
using santa::santad::EventDisposition;
using santa::santad::EventStatChangeTuple;
using santa::santad::EventStatsTuple;
using santa::santad::EventTimesTuple;
using santa::santad::FileAccessEventCountTuple;
using santa::santad::Processor;
@@ -37,6 +44,8 @@ extern NSString *const EventTypeToString(es_event_type_t eventType);
extern NSString *const EventDispositionToString(EventDisposition d);
extern NSString *const FileAccessMetricStatusToString(FileAccessMetricStatus status);
extern NSString *const FileAccessPolicyDecisionToString(FileAccessPolicyDecision decision);
extern NSString *const StatChangeStepToString(StatChangeStep decision);
extern NSString *const StatResultToString(StatResult decision);
class MetricsPeer : public Metrics {
public:
@@ -47,23 +56,50 @@ class MetricsPeer : public Metrics {
using Metrics::FlushMetrics;
// Private member variables
using Metrics::drop_cache_;
using Metrics::event_counts_cache_;
using Metrics::event_times_cache_;
using Metrics::faa_event_counts_cache_;
using Metrics::interval_;
using Metrics::rate_limit_counts_cache_;
using Metrics::running_;
using Metrics::stat_change_cache_;
using Metrics::SequenceStats;
};
} // namespace santa::santad
namespace santa::santad::event_providers::endpoint_security {
class MessagePeer : public Message {
public:
// Make base class constructors visible
using Message::Message;
// Private member variables
using Message::stat_change_step_;
using Message::stat_result_;
};
} // namespace santa::santad::event_providers::endpoint_security
using santa::santad::EventDispositionToString;
using santa::santad::EventTypeToString;
using santa::santad::FileAccessMetricStatus;
using santa::santad::FileAccessMetricStatusToString;
using santa::santad::FileAccessPolicyDecisionToString;
using santa::santad::Metrics;
using santa::santad::MetricsPeer;
using santa::santad::ProcessorToString;
using santa::santad::StatChangeStepToString;
using santa::santad::StatResultToString;
using santa::santad::event_providers::endpoint_security::MessagePeer;
std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^block)(Metrics *)) {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
return std::make_shared<MetricsPeer>(q, timer, 100, nil, nil, nil, nil, nil, nil, nil, block);
}
@interface MetricsTest : XCTestCase
@property dispatch_queue_t q;
@@ -79,11 +115,9 @@ using santa::santad::ProcessorToString;
}
- (void)testStartStop {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
^(santa::santad::Metrics *m) {
dispatch_semaphore_signal(self.sema);
});
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *) {
dispatch_semaphore_signal(self.sema);
});
XCTAssertFalse(metrics->running_);
@@ -115,10 +149,8 @@ using santa::santad::ProcessorToString;
}
- (void)testSetInterval {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
^(santa::santad::Metrics *m){
});
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
});
XCTAssertEqual(100, metrics->interval_);
@@ -164,6 +196,7 @@ using santa::santad::ProcessorToString;
{ES_EVENT_TYPE_NOTIFY_RENAME, @"NotifyRename"},
{ES_EVENT_TYPE_NOTIFY_UNLINK, @"NotifyUnlink"},
{ES_EVENT_TYPE_NOTIFY_UNMOUNT, @"NotifyUnmount"},
{ES_EVENT_TYPE_LAST, @"Global"},
};
for (const auto &kv : eventTypeToString) {
@@ -221,34 +254,72 @@ using santa::santad::ProcessorToString;
}
}
- (void)testStatChangeStepToString {
std::map<StatChangeStep, NSString *> stepToString = {
{StatChangeStep::kNoChange, @"NoChange"},
{StatChangeStep::kMessageCreate, @"MessageCreate"},
{StatChangeStep::kCodesignValidation, @"CodesignValidation"},
};
for (const auto &kv : stepToString) {
XCTAssertEqualObjects(StatChangeStepToString(kv.first), kv.second);
}
XCTAssertThrows(StatChangeStepToString((StatChangeStep)12345));
}
- (void)testStatResultToString {
std::map<StatResult, NSString *> resultToString = {
{StatResult::kOK, @"OK"},
{StatResult::kStatError, @"StatError"},
{StatResult::kDevnoInodeMismatch, @"DevnoInodeMismatch"},
};
for (const auto &kv : resultToString) {
XCTAssertEqualObjects(StatResultToString(kv.first), kv.second);
}
XCTAssertThrows(StatResultToString((StatResult)12345));
}
- (void)testSetEventMetrics {
int64_t nanos = 1234;
es_message_t esExecMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, NULL);
es_message_t esOpenMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, NULL);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage();
MessagePeer execMsg(mockESApi, &esExecMsg);
MessagePeer openMsg(mockESApi, &esOpenMsg);
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
});
// Initial maps are empty
XCTAssertEqual(metrics->event_counts_cache_.size(), 0);
XCTAssertEqual(metrics->event_times_cache_.size(), 0);
XCTAssertEqual(metrics->stat_change_cache_.size(), 0);
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
EventDisposition::kProcessed, nanos);
metrics->SetEventMetrics(Processor::kAuthorizer, EventDisposition::kProcessed, nanos, execMsg);
// Check sizes after setting metrics once
XCTAssertEqual(metrics->event_counts_cache_.size(), 1);
XCTAssertEqual(metrics->event_times_cache_.size(), 1);
XCTAssertEqual(metrics->stat_change_cache_.size(), 1);
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
EventDisposition::kProcessed, nanos);
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN,
EventDisposition::kProcessed, nanos * 2);
execMsg.stat_change_step_ = StatChangeStep::kMessageCreate;
execMsg.stat_result_ = StatResult::kDevnoInodeMismatch;
metrics->SetEventMetrics(Processor::kAuthorizer, EventDisposition::kProcessed, nanos, execMsg);
metrics->SetEventMetrics(Processor::kAuthorizer, EventDisposition::kProcessed, nanos * 2,
openMsg);
// Re-check expected counts. One was an update, so should only be 2 items
XCTAssertEqual(metrics->event_counts_cache_.size(), 2);
XCTAssertEqual(metrics->event_times_cache_.size(), 2);
// Stat change counts should be 2 because one call wasn't an AUTH EXEC event
XCTAssertEqual(metrics->stat_change_cache_.size(), 2);
// Check map values
EventCountTuple ecExec{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
@@ -257,19 +328,21 @@ using santa::santad::ProcessorToString;
EventDisposition::kProcessed};
EventTimesTuple etExec{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC};
EventTimesTuple etOpen{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN};
EventStatChangeTuple noChange{StatChangeStep::kNoChange, StatResult::kOK};
EventStatChangeTuple msgCreateChange{StatChangeStep::kMessageCreate,
StatResult::kDevnoInodeMismatch};
XCTAssertEqual(metrics->event_counts_cache_[ecExec], 2);
XCTAssertEqual(metrics->event_counts_cache_[ecOpen], 1);
XCTAssertEqual(metrics->event_times_cache_[etExec], nanos);
XCTAssertEqual(metrics->event_times_cache_[etOpen], nanos * 2);
XCTAssertEqual(metrics->stat_change_cache_[noChange], 1);
XCTAssertEqual(metrics->stat_change_cache_[msgCreateChange], 1);
}
- (void)testSetRateLimitingMetrics {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
});
// Initial map is empty
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 0);
@@ -291,11 +364,8 @@ using santa::santad::ProcessorToString;
}
- (void)testSetFileAccessEventMetrics {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
});
// Initial map is empty
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 0);
@@ -326,10 +396,71 @@ using santa::santad::ProcessorToString;
XCTAssertEqual(metrics->faa_event_counts_cache_[ruleXyz], 1);
}
- (void)testUpdateEventStats {
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXEC, NULL);
esMsg.seq_num = 0;
esMsg.global_seq_num = 0;
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
});
EventStatsTuple eventStats{Processor::kRecorder, ES_EVENT_TYPE_NOTIFY_EXEC};
EventStatsTuple globalStats{Processor::kRecorder, ES_EVENT_TYPE_LAST};
// Map does not initially contain entries
XCTAssertEqual(0, metrics->drop_cache_.size());
metrics->UpdateEventStats(Processor::kRecorder, &esMsg);
// After the first update, 2 entries exist, one for the event, and one for global
XCTAssertEqual(2, metrics->drop_cache_.size());
XCTAssertEqual(1, metrics->drop_cache_[eventStats].next_seq_num);
XCTAssertEqual(0, metrics->drop_cache_[eventStats].drops);
XCTAssertEqual(1, metrics->drop_cache_[globalStats].next_seq_num);
XCTAssertEqual(0, metrics->drop_cache_[globalStats].drops);
// Increment sequence numbers by 1 and check that no drop was detected
esMsg.seq_num++;
esMsg.global_seq_num++;
metrics->UpdateEventStats(Processor::kRecorder, &esMsg);
XCTAssertEqual(2, metrics->drop_cache_.size());
XCTAssertEqual(2, metrics->drop_cache_[eventStats].next_seq_num);
XCTAssertEqual(0, metrics->drop_cache_[eventStats].drops);
XCTAssertEqual(2, metrics->drop_cache_[globalStats].next_seq_num);
XCTAssertEqual(0, metrics->drop_cache_[globalStats].drops);
// Now incremenet sequence numbers by a large amount to trigger drop detection
esMsg.seq_num += 10;
esMsg.global_seq_num += 10;
metrics->UpdateEventStats(Processor::kRecorder, &esMsg);
XCTAssertEqual(2, metrics->drop_cache_.size());
XCTAssertEqual(12, metrics->drop_cache_[eventStats].next_seq_num);
XCTAssertEqual(9, metrics->drop_cache_[eventStats].drops);
XCTAssertEqual(12, metrics->drop_cache_[globalStats].next_seq_num);
XCTAssertEqual(9, metrics->drop_cache_[globalStats].drops);
}
- (void)testFlushMetrics {
id mockEventProcessingTimes = OCMClassMock([SNTMetricInt64Gauge class]);
id mockEventCounts = OCMClassMock([SNTMetricCounter class]);
int64_t nanos = 1234;
es_message_t esExecMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, NULL);
es_message_t esOpenMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, NULL);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage();
MessagePeer execMsg(mockESApi, &esExecMsg);
MessagePeer openMsg(mockESApi, &esOpenMsg);
// Initial update will have non-zero sequence numbers, triggering drop detection
es_message_t esMsgWithDrops = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXEC, NULL);
esMsgWithDrops.seq_num = 123;
esMsgWithDrops.global_seq_num = 123;
OCMStub([mockEventCounts incrementBy:0 forFieldValues:[OCMArg any]])
.ignoringNonObjectArgs()
@@ -344,17 +475,17 @@ using santa::santad::ProcessorToString;
});
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics =
std::make_shared<MetricsPeer>(self.q, timer, 100, mockEventProcessingTimes, mockEventCounts,
mockEventCounts, mockEventCounts, nil,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, mockEventProcessingTimes,
mockEventCounts, mockEventCounts, mockEventCounts,
mockEventCounts, mockEventCounts, nil,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
EventDisposition::kProcessed, nanos);
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN,
EventDisposition::kProcessed, nanos * 2);
metrics->SetEventMetrics(Processor::kAuthorizer, EventDisposition::kProcessed, nanos, execMsg);
metrics->SetEventMetrics(Processor::kAuthorizer, EventDisposition::kProcessed, nanos * 2,
openMsg);
metrics->UpdateEventStats(Processor::kRecorder, &esMsgWithDrops);
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 123);
metrics->SetFileAccessEventMetrics("v1.0", "rule_abc", FileAccessMetricStatus::kOK,
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied);
@@ -364,24 +495,43 @@ using santa::santad::ProcessorToString;
XCTAssertEqual(metrics->event_times_cache_.size(), 2);
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 1);
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 1);
XCTAssertEqual(metrics->stat_change_cache_.size(), 1);
XCTAssertEqual(metrics->drop_cache_.size(), 2);
EventStatsTuple eventStats{Processor::kRecorder, esMsgWithDrops.event_type};
EventStatsTuple globalStats{Processor::kRecorder, ES_EVENT_TYPE_LAST};
XCTAssertEqual(metrics->drop_cache_[eventStats].next_seq_num, 124);
XCTAssertEqual(metrics->drop_cache_[eventStats].drops, 123);
XCTAssertEqual(metrics->drop_cache_[globalStats].next_seq_num, 124);
XCTAssertEqual(metrics->drop_cache_[globalStats].drops, 123);
metrics->FlushMetrics();
// After setting two different event metrics, we expect the sema to be hit
// five times - twice each for the event counts and event times maps, and
// once for the rate limit count map.
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (1)");
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (2)");
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (3)");
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (4)");
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (5)");
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (6)");
// Expected call count is 8:
// 2: event counts
// 2: event times
// 1: stat change step
// 1: rate limit
// 1: FAA
// 2: drops (1 event, 1 global)
int expectedCalls = 9;
for (int i = 0; i < expectedCalls; i++) {
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush");
}
// After a flush, map sizes should be reset to 0
XCTAssertEqual(metrics->event_counts_cache_.size(), 0);
XCTAssertEqual(metrics->event_times_cache_.size(), 0);
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 0);
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 0);
XCTAssertEqual(metrics->stat_change_cache_.size(), 0);
// Note: The drop_cache_ should not be reset back to size 0. Instead, each
// entry has the sequence number left intact, but drop counts reset to 0.
XCTAssertEqual(metrics->drop_cache_.size(), 2);
XCTAssertEqual(metrics->drop_cache_[eventStats].next_seq_num, 124);
XCTAssertEqual(metrics->drop_cache_[eventStats].drops, 0);
XCTAssertEqual(metrics->drop_cache_[globalStats].next_seq_num, 124);
XCTAssertEqual(metrics->drop_cache_[globalStats].drops, 0);
}
@end

View File

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

View File

@@ -0,0 +1,33 @@
/// Copyright 2023 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#ifndef SANTA__SANTAD_PROCESSTREE_SNTENDPOINTSECURITYADAPTER_H
#define SANTA__SANTAD_PROCESSTREE_SNTENDPOINTSECURITYADAPTER_H
#include <EndpointSecurity/EndpointSecurity.h>
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/ProcessTree/process_tree.h"
namespace santa::santad::process_tree {
// Inform the tree of the ES event in msg.
// This is idempotent on the tree, so can be called from multiple places with
// the same msg.
void InformFromESEvent(
ProcessTree &tree,
const santa::santad::event_providers::endpoint_security::Message &msg);
} // namespace santa::santad::process_tree
#endif

View File

@@ -0,0 +1,73 @@
/// Copyright 2023 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include "Source/santad/ProcessTree/SNTEndpointSecurityAdapter.h"
#include <EndpointSecurity/EndpointSecurity.h>
#include <Foundation/Foundation.h>
#include <bsm/libbsm.h>
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/ProcessTree/process_tree.h"
#include "Source/santad/ProcessTree/process_tree_macos.h"
#include "absl/status/statusor.h"
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::Message;
namespace santa::santad::process_tree {
void InformFromESEvent(ProcessTree &tree, const Message &msg) {
struct Pid event_pid = PidFromAuditToken(msg->process->audit_token);
auto proc = tree.Get(event_pid);
if (!proc) {
return;
}
std::shared_ptr<EndpointSecurityAPI> esapi = msg.ESAPI();
switch (msg->event_type) {
case ES_EVENT_TYPE_AUTH_EXEC:
case ES_EVENT_TYPE_NOTIFY_EXEC: {
std::vector<std::string> args;
args.reserve(esapi->ExecArgCount(&msg->event.exec));
for (int i = 0; i < esapi->ExecArgCount(&msg->event.exec); i++) {
es_string_token_t arg = esapi->ExecArg(&msg->event.exec, i);
args.push_back(std::string(arg.data, arg.length));
}
es_string_token_t executable = msg->event.exec.target->executable->path;
tree.HandleExec(
msg->mach_time, **proc, PidFromAuditToken(msg->event.exec.target->audit_token),
(struct Program){.executable = std::string(executable.data, executable.length),
.arguments = args},
(struct Cred){
.uid = audit_token_to_euid(msg->event.exec.target->audit_token),
.gid = audit_token_to_egid(msg->event.exec.target->audit_token),
});
break;
}
case ES_EVENT_TYPE_NOTIFY_FORK: {
tree.HandleFork(msg->mach_time, **proc,
PidFromAuditToken(msg->event.fork.child->audit_token));
break;
}
case ES_EVENT_TYPE_NOTIFY_EXIT: tree.HandleExit(msg->mach_time, **proc); break;
default: return;
}
}
} // namespace santa::santad::process_tree

View File

@@ -0,0 +1,37 @@
load("//:helper.bzl", "santa_unit_test")
package(
default_visibility = ["//:santa_package_group"],
)
cc_library(
name = "annotator",
hdrs = ["annotator.h"],
deps = [
"//Source/santad/ProcessTree:process_tree_cc_proto",
],
)
cc_library(
name = "originator",
srcs = ["originator.cc"],
hdrs = ["originator.h"],
deps = [
":annotator",
"//Source/santad/ProcessTree:process",
"//Source/santad/ProcessTree:process_tree",
"//Source/santad/ProcessTree:process_tree_cc_proto",
"@com_google_absl//absl/container:flat_hash_map",
],
)
santa_unit_test(
name = "originator_test",
srcs = ["originator_test.mm"],
deps = [
":originator",
"//Source/santad/ProcessTree:process",
"//Source/santad/ProcessTree:process_tree_cc_proto",
"//Source/santad/ProcessTree:process_tree_test_helpers",
],
)

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