mirror of
https://github.com/google/santa.git
synced 2026-01-15 01:08:12 -05:00
Compare commits
148 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70474aba3e | ||
|
|
f4ad76b974 | ||
|
|
3b7061ea62 | ||
|
|
280d93ee08 | ||
|
|
f73463117f | ||
|
|
f93e1a56a0 | ||
|
|
d5195b55d2 | ||
|
|
15e5874d43 | ||
|
|
5e6fa09f1c | ||
|
|
ce2777ae94 | ||
|
|
f8a20d35b4 | ||
|
|
2e69370524 | ||
|
|
f9b4e00e0c | ||
|
|
e2e83a099c | ||
|
|
2cbf15566a | ||
|
|
1596990c65 | ||
|
|
221664436f | ||
|
|
65c660298c | ||
|
|
2b5d55781c | ||
|
|
84e6d6ccff | ||
|
|
c16f90f5f9 | ||
|
|
d503eae4d9 | ||
|
|
818518bb38 | ||
|
|
f499654951 | ||
|
|
a5e8d77d06 | ||
|
|
edac42e8b8 | ||
|
|
ce5e3d0ee4 | ||
|
|
3e51ec6b8a | ||
|
|
ed227f43d4 | ||
|
|
056ed75bf1 | ||
|
|
8f5f8de245 | ||
|
|
7c58648c35 | ||
|
|
3f3751eb18 | ||
|
|
7aa2d69ce6 | ||
|
|
f9a937a6e4 | ||
|
|
d2cbddd3fb | ||
|
|
ea7e11fc22 | ||
|
|
7530b8f5c1 | ||
|
|
64bb34b2ca | ||
|
|
c5c6037085 | ||
|
|
275a8ed607 | ||
|
|
28dd6cbaed | ||
|
|
8c466b4408 | ||
|
|
373c676306 | ||
|
|
d214d510e5 | ||
|
|
6314fe04e3 | ||
|
|
11d9c29daa | ||
|
|
60238f0ed2 | ||
|
|
7aa731a76f | ||
|
|
5a383ebd9a | ||
|
|
913af692e8 | ||
|
|
4d6140d047 | ||
|
|
2edd2ddfd2 | ||
|
|
1515929752 | ||
|
|
fc2c7ffb71 | ||
|
|
98ee36850a | ||
|
|
6f4a48866c | ||
|
|
51ca19b238 | ||
|
|
b8d7ed0c07 | ||
|
|
ff6bf0701d | ||
|
|
3be45fd6c0 | ||
|
|
d2e5aec635 | ||
|
|
be1169ffcb | ||
|
|
181c3ae573 | ||
|
|
5f0755efbf | ||
|
|
f0165089a4 | ||
|
|
5c98ef6897 | ||
|
|
e2f8ca9569 | ||
|
|
2029e239ca | ||
|
|
cae3578b62 | ||
|
|
16a8c651d5 | ||
|
|
4fdc1e5e41 | ||
|
|
1cdd04f9eb | ||
|
|
4d0af8838f | ||
|
|
0400e29264 | ||
|
|
2c6da7158d | ||
|
|
b0ab761568 | ||
|
|
b02336613a | ||
|
|
bd86145679 | ||
|
|
6dfd5ba084 | ||
|
|
72e292d80e | ||
|
|
6588c2342b | ||
|
|
d82e64aa5f | ||
|
|
a9c1c730be | ||
|
|
6c4362d8bb | ||
|
|
c1189493e8 | ||
|
|
aaa0d40841 | ||
|
|
a424c4afca | ||
|
|
2847397b66 | ||
|
|
ad8b4b6646 | ||
|
|
39ee9e7d48 | ||
|
|
3cccacc3fb | ||
|
|
6ed5bcd808 | ||
|
|
bcac65a23e | ||
|
|
03fcd0c906 | ||
|
|
d3b71a3ba8 | ||
|
|
9e124f4c51 | ||
|
|
cd719ccef4 | ||
|
|
dde42ee686 | ||
|
|
d144e27798 | ||
|
|
afc2c216b8 | ||
|
|
03d7556f22 | ||
|
|
020827b091 | ||
|
|
baa31a5db0 | ||
|
|
9ba7075596 | ||
|
|
5d08538639 | ||
|
|
e73bafb596 | ||
|
|
1e92d109a7 | ||
|
|
6a6aa6dce8 | ||
|
|
0715033d6a | ||
|
|
123d7a2d6a | ||
|
|
7b4d997589 | ||
|
|
5307bd9b7f | ||
|
|
0622e6de71 | ||
|
|
e7c32ae87d | ||
|
|
deaf3a638c | ||
|
|
8a7f1142a8 | ||
|
|
c180205059 | ||
|
|
337df0aa31 | ||
|
|
e2b099aa50 | ||
|
|
fc4e29f34c | ||
|
|
bf3b6bc6e2 | ||
|
|
b810fc81e1 | ||
|
|
3b3aa999c5 | ||
|
|
59428f3be3 | ||
|
|
ae6451a9b2 | ||
|
|
feac080fa7 | ||
|
|
d0f2a0ac4d | ||
|
|
7fc06ea9d8 | ||
|
|
1dfeeac936 | ||
|
|
ac9b5d9399 | ||
|
|
7f3f1c5448 | ||
|
|
46efd6893f | ||
|
|
50232578d6 | ||
|
|
d83be03a20 | ||
|
|
119b29b534 | ||
|
|
be87b3eaf2 | ||
|
|
0fe672817e | ||
|
|
c3b2fbf512 | ||
|
|
2984d98cb9 | ||
|
|
5295faef0e | ||
|
|
0209344f62 | ||
|
|
53ca5eb811 | ||
|
|
33c7aab9f1 | ||
|
|
f6d837ac31 | ||
|
|
5e0a383662 | ||
|
|
8055b451bb | ||
|
|
c5e7736eef |
11
.bazelrc
11
.bazelrc
@@ -3,8 +3,13 @@ build --apple_generate_dsym --define=apple.propagate_embedded_extra_outputs=yes
|
||||
build --copt=-Werror
|
||||
build --copt=-Wall
|
||||
build --copt=-Wno-error=deprecated-declarations
|
||||
# Disable -Wunknown-warning-option because deprecated-non-prototype
|
||||
# isn't recognized on older SDKs
|
||||
build --copt=-Wno-unknown-warning-option
|
||||
build --copt=-Wno-error=deprecated-non-prototype
|
||||
build --per_file_copt=.*\.mm\$@-std=c++17
|
||||
build --cxxopt=-std=c++17
|
||||
build --host_cxxopt=-std=c++17
|
||||
|
||||
build --copt=-DSANTA_OPEN_SOURCE=1
|
||||
build --cxxopt=-DSANTA_OPEN_SOURCE=1
|
||||
@@ -21,20 +26,20 @@ build:asan --config=san-common
|
||||
build:asan --copt="-fsanitize=address"
|
||||
build:asan --copt="-DADDRESS_SANITIZER"
|
||||
build:asan --linkopt="-fsanitize=address"
|
||||
build:asan --action_env="ASAN_OPTIONS=log_path=/tmp/san_out"
|
||||
build:asan --test_env="ASAN_OPTIONS=log_path=/tmp/san_out"
|
||||
|
||||
build:tsan --config=san-common
|
||||
build:tsan --copt="-fsanitize=thread"
|
||||
build:tsan --copt="-DTHREAD_SANITIZER=1"
|
||||
build:tsan --linkopt="-fsanitize=thread"
|
||||
build:asan --action_env="TSAN_OPTIONS=log_path=/tmp/san_out"
|
||||
build:tsan --test_env="TSAN_OPTIONS=log_path=/tmp/san_out:halt_on_error=true"
|
||||
|
||||
build:ubsan --config=san-common
|
||||
build:ubsan --copt="-fsanitize=undefined"
|
||||
build:ubsan --copt="-DUNDEFINED_SANITIZER=1"
|
||||
build:ubsan --copt="-fno-sanitize=function" --copt="-fno-sanitize=vptr"
|
||||
build:ubsan --linkopt="-fsanitize=undefined"
|
||||
build:ubsan --action_env="UBSAN_OPTIONS=log_path=/tmp/san_out"
|
||||
build:ubsan --test_env="UBSAN_OPTIONS=log_path=/tmp/san_out"
|
||||
|
||||
build:fuzz --config=san-common
|
||||
build:fuzz --@rules_fuzzing//fuzzing:cc_engine=@rules_fuzzing//fuzzing/engines:libfuzzer
|
||||
|
||||
@@ -1 +1 @@
|
||||
5.3.0
|
||||
6.3.2
|
||||
|
||||
9
.github/workflows/check-markdown.yml
vendored
9
.github/workflows/check-markdown.yml
vendored
@@ -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'"
|
||||
|
||||
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
@@ -1,5 +1,4 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -11,45 +10,41 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- 'Source/**'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Run linters
|
||||
run: ./Testing/lint.sh
|
||||
|
||||
build_userspace:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-11, macos-12]
|
||||
os: [macos-11, macos-12, macos-13]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Build Userspace
|
||||
run: bazel build --apple_generate_dsym -c opt :release --define=SANTA_BUILD_TYPE=adhoc
|
||||
|
||||
unit_tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-11, macos-12]
|
||||
os: [macos-11, macos-12, macos-13]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- 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
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Generate test coverage
|
||||
run: sh ./generate_cov.sh
|
||||
- name: Coveralls
|
||||
uses: coverallsapp/github-action@master
|
||||
uses: coverallsapp/github-action@09b709cf6a16e30b0808ba050c7a6e8a5ef13f8d # ratchet:coverallsapp/github-action@master
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path-to-lcov: ./bazel-out/_coverage/_coverage_report.dat
|
||||
|
||||
2
.github/workflows/continuous.yml
vendored
2
.github/workflows/continuous.yml
vendored
@@ -8,6 +8,6 @@ jobs:
|
||||
preqs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checks for flaky tests
|
||||
run: bazel test --test_strategy=exclusive --test_output=errors --runs_per_test 50 -t- :unit_tests --define=SANTA_BUILD_TYPE=adhoc
|
||||
|
||||
34
.github/workflows/e2e.yml
vendored
34
.github/workflows/e2e.yml
vendored
@@ -1,41 +1,49 @@
|
||||
name: E2E
|
||||
|
||||
on: workflow_dispatch
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 4 * * *' # Every day at 4:00 UTC (not to interfere with fuzzing)
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
start_vm:
|
||||
runs-on: e2e-host
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Start VM
|
||||
run: python3 Testing/integration/actions/start_vm.py macOS_12.bundle.tar.gz
|
||||
run: python3 Testing/integration/actions/start_vm.py macOS_14.bundle.tar.gz
|
||||
|
||||
integration:
|
||||
runs-on: e2e-vm
|
||||
env:
|
||||
VM_PASSWORD: ${{ secrets.VM_PASSWORD }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install configuration profile
|
||||
run: bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Add homebrew to PATH
|
||||
run: echo "/opt/homebrew/bin/" >> $GITHUB_PATH
|
||||
- name: Install configuration profile
|
||||
run: bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig
|
||||
- name: Build, install, and sync santa
|
||||
run: |
|
||||
bazel run :reload --define=SANTA_BUILD_TYPE=adhoc
|
||||
bazel run //Testing/integration:allow_sysex
|
||||
- name: Test config changes
|
||||
run: ./Testing/integration/test_config_changes.sh
|
||||
- name: Build, install, and start moroz
|
||||
run: |
|
||||
bazel build @com_github_groob_moroz//cmd/moroz:moroz
|
||||
cp bazel-bin/external/com_github_groob_moroz/cmd/moroz/moroz_/moroz /tmp/moroz
|
||||
/tmp/moroz -configs="$GITHUB_WORKSPACE/Testing/integration/configs/moroz_default/global.toml" -use-tls=false &
|
||||
- name: Build, install, and sync santa
|
||||
run: |
|
||||
bazel run :reload --define=SANTA_BUILD_TYPE=adhoc
|
||||
bazel run //Testing/integration:allow_sysex
|
||||
sudo santactl sync --debug
|
||||
- name: Run integration test binaries
|
||||
run: bazel test //Testing/integration:integration_tests
|
||||
- name: Test config changes
|
||||
run: ./Testing/integration/test_config_changes.sh
|
||||
run: |
|
||||
bazel test //Testing/integration:integration_tests
|
||||
sleep 3
|
||||
bazel run //Testing/integration:dismiss_santa_popup || true
|
||||
- name: Test sync server changes
|
||||
run: ./Testing/integration/test_sync_changes.sh
|
||||
- name: Test USB blocking
|
||||
run: ./Testing/integration/test_usb.sh
|
||||
- name: Poweroff
|
||||
if: ${{ always() }}
|
||||
run: sudo shutdown -h +1
|
||||
|
||||
4
.github/workflows/fuzz.yml
vendored
4
.github/workflows/fuzz.yml
vendored
@@ -9,14 +9,14 @@ jobs:
|
||||
start_vm:
|
||||
runs-on: e2e-host
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Start VM
|
||||
run: python3 Testing/integration/actions/start_vm.py macOS_13.bundle.tar.gz
|
||||
|
||||
fuzz:
|
||||
runs-on: e2e-vm
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup libfuzzer
|
||||
run: Fuzzing/install_libclang_fuzzer.sh
|
||||
- name: Fuzz
|
||||
|
||||
4
.github/workflows/sanitizers.yml
vendored
4
.github/workflows/sanitizers.yml
vendored
@@ -11,14 +11,14 @@ jobs:
|
||||
matrix:
|
||||
sanitizer: [asan, tsan, ubsan]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: ${{ matrix.sanitizer }}
|
||||
run: |
|
||||
CLANG_VERSION=$(clang --version | head -n 1 | cut -d' ' -f 4)
|
||||
DYLIB_PATH="$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/${CLANG_VERSION}/lib/darwin/libclang_rt.${{ matrix.sanitizer }}_osx_dynamic.dylib"
|
||||
|
||||
bazel test --config=${{ matrix.sanitizer }} \
|
||||
--test_strategy=exclusive --test_output=errors \
|
||||
--test_strategy=exclusive --test_output=all \
|
||||
--test_env=DYLD_INSERT_LIBRARIES=${DYLIB_PATH} \
|
||||
--runs_per_test 5 -t- :unit_tests \
|
||||
--define=SANTA_BUILD_TYPE=adhoc
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
# Example NOTARIZATION_TOOL wrapper.
|
||||
|
||||
/usr/bin/xcrun altool --notarize-app "${2}" --primary-bundle-id "${4}" \
|
||||
-u "${NOTARIZATION_USERNAME}" -p "${NOTARIZATION_PASSWORD}"
|
||||
/usr/bin/xcrun notarytool submit "${2}" --wait \
|
||||
--apple-id "${NOTARIZATION_USERNAME}" --password "${NOTARIZATION_PASSWORD}"
|
||||
|
||||
@@ -28,8 +28,6 @@
|
||||
# tool around the tool to use for notarization. The tool must take 2 flags:
|
||||
# --file
|
||||
# - pointing at a zip file containing the artifact to notarize
|
||||
# --primary-bundle-id
|
||||
# - specifying the CFBundleID of the artifact being notarized
|
||||
[[ -n "${NOTARIZATION_TOOL}" ]] || die "NOTARIZATION_TOOL unset"
|
||||
|
||||
# ARTIFACTS_DIR is a required environment variable pointing at a directory to
|
||||
@@ -92,7 +90,7 @@ for ARTIFACT in "${INPUT_SYSX}" "${INPUT_APP}"; do
|
||||
|
||||
echo "notarizing ${BN}"
|
||||
PBID=$(/usr/bin/defaults read "${ARTIFACT}/Contents/Info.plist" CFBundleIdentifier)
|
||||
"${NOTARIZATION_TOOL}" --file "${SCRATCH}/${BN}.zip" --primary-bundle-id "${PBID}"
|
||||
"${NOTARIZATION_TOOL}" --file "${SCRATCH}/${BN}.zip"
|
||||
done
|
||||
|
||||
# Staple the App.
|
||||
@@ -166,8 +164,7 @@ echo "verifying pkg signature"
|
||||
/usr/sbin/pkgutil --check-signature "${SCRATCH}/${RELEASE_NAME}/${RELEASE_NAME}.pkg" || die "bad pkg signature"
|
||||
|
||||
echo "notarizing pkg"
|
||||
"${NOTARIZATION_TOOL}" --file "${SCRATCH}/${RELEASE_NAME}/${RELEASE_NAME}.pkg" \
|
||||
--primary-bundle-id "com.google.santa"
|
||||
"${NOTARIZATION_TOOL}" --file "${SCRATCH}/${RELEASE_NAME}/${RELEASE_NAME}.pkg"
|
||||
|
||||
echo "stapling pkg"
|
||||
/usr/bin/xcrun stapler staple "${SCRATCH}/${RELEASE_NAME}/${RELEASE_NAME}.pkg" || die "failed to staple pkg"
|
||||
@@ -179,7 +176,7 @@ echo "wrapping pkg in dmg"
|
||||
-srcfolder "${SCRATCH}/${RELEASE_NAME}/" "${DMG_PATH}" || die "failed to wrap pkg in dmg"
|
||||
|
||||
echo "notarizing dmg"
|
||||
"${NOTARIZATION_TOOL}" --file "${DMG_PATH}" --primary-bundle-id "com.google.santa"
|
||||
"${NOTARIZATION_TOOL}" --file "${DMG_PATH}"
|
||||
|
||||
echo "stapling dmg"
|
||||
/usr/bin/xcrun stapler staple "${DMG_PATH}" || die "failed to staple dmg"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
9
LICENSE
9
LICENSE
@@ -201,12 +201,3 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
------------------
|
||||
|
||||
Files: Testing/integration/VM/*
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
18
README.md
18
README.md
@@ -1,10 +1,16 @@
|
||||
# Santa [](https://github.com/google/santa/actions/workflows/ci.yml)
|
||||
# Santa
|
||||
|
||||
[](https://github.com/google/santa/blob/main/LICENSE)
|
||||
[](https://github.com/google/santa/actions/workflows/ci.yml)
|
||||
[](https://github.com/google/santa/releases/latest)
|
||||
[](https://github.com/google/santa/releases/latest)
|
||||
[](https://github.com/google/santa/releases/latest)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/google/santa/main/Source/gui/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
|
||||
<img src="./docs/images/santa-sleigh-256.png" height="128" alt="Santa Icon" />
|
||||
</p>
|
||||
|
||||
Santa is a binary authorization system for macOS. It consists of a system
|
||||
Santa is a binary and file access authorization system for macOS. It consists of a system
|
||||
extension that monitors for executions, a daemon that makes execution decisions
|
||||
based on the contents of a local database, a GUI agent that notifies the user in
|
||||
case of a block decision and a command-line utility for managing the system and
|
||||
@@ -42,9 +48,7 @@ disclosure reporting.
|
||||
the events database. In LOCKDOWN mode, only listed binaries are allowed to
|
||||
run.
|
||||
|
||||
* Event logging: When the kext is loaded, all binary launches are logged. When
|
||||
in either mode, all unknown or denied binaries are stored in the database to
|
||||
enable later aggregation.
|
||||
* Event logging: When the system extension is loaded, all binary launches are logged. When in either mode, all unknown or denied binaries are stored in the database to enable later aggregation.
|
||||
|
||||
* Certificate-based rules, with override levels: Instead of relying on a
|
||||
binary's hash (or 'fingerprint'), executables can be allowed/blocked by their
|
||||
@@ -134,7 +138,7 @@ A tool like Santa doesn't really lend itself to screenshots, so here's a video
|
||||
instead.
|
||||
|
||||
|
||||
<p align="center"> <img src="https://thumbs.gfycat.com/MadFatalAmphiuma-small.gif" alt="Santa Block Video" /> </p>
|
||||
<p align="center"> <img src="./docs/images/santa-block.gif" alt="Santa Block Video" /> </p>
|
||||
|
||||
# Contributing
|
||||
Patches to this project are very much welcome. Please see the
|
||||
|
||||
14
SECURITY.md
14
SECURITY.md
@@ -1,12 +1,14 @@
|
||||
# Reporting a Vulnerability
|
||||
|
||||
If you believe you have found a security vulnerability, we would appreciate private disclosure
|
||||
so that we can work on a fix before disclosure. Any vulnerabilities reported to us will be
|
||||
If you believe you have found a security vulnerability, we would appreciate a private report
|
||||
so that we can work on and release a fix before public disclosure. Any vulnerabilities reported to us will be
|
||||
disclosed publicly either when a new version with fixes is released or 90 days has passed,
|
||||
whichever comes first.
|
||||
|
||||
To report vulnerabilities to us privately, please e-mail `santa-team@google.com`.
|
||||
If you want to encrypt your e-mail, you can use our GPG key `0x92AFE41DAB49BBB6`
|
||||
available on keyserver.ubuntu.com:
|
||||
To report vulnerabilities to us privately, either:
|
||||
|
||||
`gpg --keyserver keyserver.ubuntu.com --recv-key 0x92AFE41DAB49BBB6`
|
||||
1) Report the vulnerability [through GitHub](https://github.com/google/santa/security/advisories/new).
|
||||
|
||||
2) E-mail `santa-team@google.com`. If you want to encrypt your e-mail, you can use our GPG key `0x92AFE41DAB49BBB6` available on keyserver.ubuntu.com:
|
||||
|
||||
`gpg --keyserver keyserver.ubuntu.com --recv-key 0x92AFE41DAB49BBB6`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//:helper.bzl", "santa_unit_test")
|
||||
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
|
||||
load("//:helper.bzl", "santa_unit_test")
|
||||
|
||||
package(
|
||||
default_visibility = ["//:santa_package_group"],
|
||||
@@ -40,6 +40,12 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTDeepCopy",
|
||||
srcs = ["SNTDeepCopy.m"],
|
||||
hdrs = ["SNTDeepCopy.h"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "SantaCache",
|
||||
hdrs = ["SantaCache.h"],
|
||||
@@ -54,6 +60,56 @@ santa_unit_test(
|
||||
],
|
||||
)
|
||||
|
||||
# This target shouldn't be used directly.
|
||||
# Use a more specific scoped type instead.
|
||||
objc_library(
|
||||
name = "ScopedTypeRef",
|
||||
hdrs = ["ScopedTypeRef.h"],
|
||||
visibility = ["//Source/common:__pkg__"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "ScopedCFTypeRef",
|
||||
hdrs = ["ScopedCFTypeRef.h"],
|
||||
deps = [
|
||||
":ScopedTypeRef",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "ScopedCFTypeRefTest",
|
||||
srcs = ["ScopedCFTypeRefTest.mm"],
|
||||
sdk_frameworks = [
|
||||
"Security",
|
||||
],
|
||||
deps = [
|
||||
":ScopedCFTypeRef",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "ScopedIOObjectRef",
|
||||
hdrs = ["ScopedIOObjectRef.h"],
|
||||
sdk_frameworks = [
|
||||
"IOKit",
|
||||
],
|
||||
deps = [
|
||||
":ScopedTypeRef",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "ScopedIOObjectRefTest",
|
||||
srcs = ["ScopedIOObjectRefTest.mm"],
|
||||
sdk_frameworks = [
|
||||
"IOKit",
|
||||
],
|
||||
deps = [
|
||||
":ScopedIOObjectRef",
|
||||
"//Source/santad:EndpointSecuritySerializerUtilities",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "BranchPrediction",
|
||||
hdrs = ["BranchPrediction.h"],
|
||||
@@ -69,6 +125,11 @@ objc_library(
|
||||
hdrs = ["Platform.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "String",
|
||||
hdrs = ["String.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SantaVnodeHash",
|
||||
srcs = ["SantaVnodeHash.mm"],
|
||||
@@ -79,12 +140,22 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "CertificateHelpers",
|
||||
srcs = ["CertificateHelpers.m"],
|
||||
hdrs = ["CertificateHelpers.h"],
|
||||
deps = [
|
||||
"@MOLCertificate",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTBlockMessage",
|
||||
srcs = ["SNTBlockMessage.m"],
|
||||
hdrs = ["SNTBlockMessage.h"],
|
||||
deps = [
|
||||
":SNTConfigurator",
|
||||
":SNTFileAccessEvent",
|
||||
":SNTLogging",
|
||||
":SNTStoredEvent",
|
||||
":SNTSystemInfo",
|
||||
@@ -98,7 +169,7 @@ objc_library(
|
||||
defines = ["SANTAGUI"],
|
||||
deps = [
|
||||
":SNTConfigurator",
|
||||
":SNTDeviceEvent",
|
||||
":SNTFileAccessEvent",
|
||||
":SNTLogging",
|
||||
":SNTStoredEvent",
|
||||
":SNTSystemInfo",
|
||||
@@ -128,6 +199,20 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTFileAccessEvent",
|
||||
srcs = ["SNTFileAccessEvent.m"],
|
||||
hdrs = ["SNTFileAccessEvent.h"],
|
||||
module_name = "santa_common_SNTFileAccessEvent",
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
],
|
||||
deps = [
|
||||
":CertificateHelpers",
|
||||
"@MOLCertificate",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTCommonEnums",
|
||||
textual_hdrs = ["SNTCommonEnums.h"],
|
||||
@@ -220,7 +305,11 @@ objc_library(
|
||||
santa_unit_test(
|
||||
name = "SNTRuleTest",
|
||||
srcs = ["SNTRuleTest.m"],
|
||||
deps = [":SNTRule"],
|
||||
deps = [
|
||||
":SNTCommonEnums",
|
||||
":SNTRule",
|
||||
":SNTSyncConstants",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
@@ -375,16 +464,43 @@ santa_unit_test(
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTBlockMessageTest",
|
||||
srcs = ["SNTBlockMessageTest.m"],
|
||||
deps = [
|
||||
":SNTBlockMessage",
|
||||
":SNTConfigurator",
|
||||
":SNTFileAccessEvent",
|
||||
":SNTStoredEvent",
|
||||
":SNTSystemInfo",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTConfiguratorTest",
|
||||
srcs = ["SNTConfiguratorTest.m"],
|
||||
deps = [
|
||||
":SNTCommonEnums",
|
||||
":SNTConfigurator",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "unit_tests",
|
||||
tests = [
|
||||
":PrefixTreeTest",
|
||||
":SNTBlockMessageTest",
|
||||
":SNTCachedDecisionTest",
|
||||
":SNTConfiguratorTest",
|
||||
":SNTFileInfoTest",
|
||||
":SNTKVOManagerTest",
|
||||
":SNTMetricSetTest",
|
||||
":SNTRuleTest",
|
||||
":SantaCacheTest",
|
||||
":ScopedCFTypeRefTest",
|
||||
":ScopedIOObjectRefTest",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
43
Source/common/CertificateHelpers.h
Normal file
43
Source/common/CertificateHelpers.h
Normal file
@@ -0,0 +1,43 @@
|
||||
/// 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>
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#include <sys/cdefs.h>
|
||||
|
||||
__BEGIN_DECLS
|
||||
|
||||
/**
|
||||
Return a string representing publisher info from the provided certs
|
||||
|
||||
@param certs A certificate chain
|
||||
@param teamID A team ID to be displayed for apps from the App Store
|
||||
|
||||
@return A string that tries to be more helpful to users by extracting
|
||||
appropriate information from the certificate chain.
|
||||
*/
|
||||
NSString *Publisher(NSArray<MOLCertificate *> *certs, NSString *teamID);
|
||||
|
||||
/**
|
||||
Return an array of the underlying SecCertificateRef's for the given array
|
||||
of MOLCertificates.
|
||||
|
||||
@param certs An array of MOLCertificates
|
||||
|
||||
@return An array of SecCertificateRefs. WARNING: If the refs need to be used
|
||||
for a long time be careful to properly CFRetain/CFRelease the returned items.
|
||||
*/
|
||||
NSArray<id> *CertificateChain(NSArray<MOLCertificate *> *certs);
|
||||
|
||||
__END_DECLS
|
||||
42
Source/common/CertificateHelpers.m
Normal file
42
Source/common/CertificateHelpers.m
Normal file
@@ -0,0 +1,42 @@
|
||||
/// 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/CertificateHelpers.h"
|
||||
|
||||
#include <Security/SecCertificate.h>
|
||||
|
||||
NSString *Publisher(NSArray<MOLCertificate *> *certs, NSString *teamID) {
|
||||
MOLCertificate *leafCert = [certs firstObject];
|
||||
|
||||
if ([leafCert.commonName isEqualToString:@"Apple Mac OS Application Signing"]) {
|
||||
return [NSString stringWithFormat:@"App Store (Team ID: %@)", teamID];
|
||||
} else if (leafCert.commonName && leafCert.orgName) {
|
||||
return [NSString stringWithFormat:@"%@ - %@", leafCert.orgName, leafCert.commonName];
|
||||
} else if (leafCert.commonName) {
|
||||
return leafCert.commonName;
|
||||
} else if (leafCert.orgName) {
|
||||
return leafCert.orgName;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSArray<id> *CertificateChain(NSArray<MOLCertificate *> *certs) {
|
||||
NSMutableArray *certArray = [NSMutableArray arrayWithCapacity:[certs count]];
|
||||
for (MOLCertificate *cert in certs) {
|
||||
[certArray addObject:(id)cert.certRef];
|
||||
}
|
||||
|
||||
return certArray;
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -191,12 +191,12 @@ using santa::common::PrefixTree;
|
||||
uint32_t count = 4096;
|
||||
auto t = new PrefixTree<int>(count * (uint32_t)[NSUUID UUID].UUIDString.length);
|
||||
|
||||
NSMutableArray *UUIDs = [NSMutableArray arrayWithCapacity:count];
|
||||
__block NSMutableArray *UUIDs = [NSMutableArray arrayWithCapacity:count];
|
||||
for (int i = 0; i < count; ++i) {
|
||||
[UUIDs addObject:[NSUUID UUID].UUIDString];
|
||||
}
|
||||
|
||||
__block BOOL stop = NO;
|
||||
__block _Atomic BOOL stop = NO;
|
||||
|
||||
// Create a bunch of background noise.
|
||||
dispatch_async(dispatch_get_global_queue(0, 0), ^{
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#endif
|
||||
|
||||
#import "Source/common/SNTFileAccessEvent.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
|
||||
@class SNTStoredEvent;
|
||||
|
||||
@interface SNTBlockMessage : NSObject
|
||||
@@ -38,11 +41,15 @@
|
||||
+ (NSAttributedString *)attributedBlockMessageForEvent:(SNTStoredEvent *)event
|
||||
customMessage:(NSString *)customMessage;
|
||||
|
||||
+ (NSAttributedString *)attributedBlockMessageForFileAccessEvent:(SNTFileAccessEvent *)event
|
||||
customMessage:(NSString *)customMessage;
|
||||
|
||||
///
|
||||
/// Return a URL generated from the EventDetailURL configuration key
|
||||
/// after replacing templates in the URL with values from the event.
|
||||
///
|
||||
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event;
|
||||
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event customURL:(NSString *)url;
|
||||
+ (NSURL *)eventDetailURLForFileAccessEvent:(SNTFileAccessEvent *)event customURL:(NSString *)url;
|
||||
|
||||
///
|
||||
/// Strip HTML from a string, replacing <br /> with newline.
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTFileAccessEvent.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/common/SNTSystemInfo.h"
|
||||
@@ -82,6 +83,18 @@
|
||||
return [SNTBlockMessage formatMessage:message];
|
||||
}
|
||||
|
||||
+ (NSAttributedString *)attributedBlockMessageForFileAccessEvent:(SNTFileAccessEvent *)event
|
||||
customMessage:(NSString *)customMessage {
|
||||
NSString *message = customMessage;
|
||||
if (!message.length) {
|
||||
message = [[SNTConfigurator configurator] fileAccessBlockMessage];
|
||||
if (!message.length) {
|
||||
message = @"Access to a file has been denied.";
|
||||
}
|
||||
}
|
||||
return [SNTBlockMessage formatMessage:message];
|
||||
}
|
||||
|
||||
+ (NSString *)stringFromHTML:(NSString *)html {
|
||||
NSError *error;
|
||||
NSXMLDocument *xml = [[NSXMLDocument alloc] initWithXMLString:html options:0 error:&error];
|
||||
@@ -109,46 +122,127 @@
|
||||
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event {
|
||||
+ (NSString *)replaceFormatString:(NSString *)str
|
||||
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];
|
||||
}
|
||||
}];
|
||||
|
||||
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:
|
||||
//
|
||||
// %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.
|
||||
// %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 {
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
|
||||
NSString *hostname = [SNTSystemInfo longHostname];
|
||||
NSString *uuid = [SNTSystemInfo hardwareUUID];
|
||||
NSString *serial = [SNTSystemInfo serialNumber];
|
||||
NSString *formatStr = config.eventDetailURL;
|
||||
if (!formatStr.length) return nil;
|
||||
|
||||
if (event.fileSHA256) {
|
||||
// This key is deprecated, use %file_identifier% or %bundle_or_file_identifier%
|
||||
formatStr =
|
||||
[formatStr stringByReplacingOccurrencesOfString:@"%file_sha%"
|
||||
withString:event.fileBundleHash ?: event.fileSHA256];
|
||||
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%file_identifier%"
|
||||
withString:event.fileSHA256];
|
||||
formatStr =
|
||||
[formatStr stringByReplacingOccurrencesOfString:@"%bundle_or_file_identifier%"
|
||||
withString:event.fileBundleHash ?: event.fileSHA256];
|
||||
}
|
||||
if (event.executingUser) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%username%"
|
||||
withString:event.executingUser];
|
||||
}
|
||||
if (config.machineID) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%machine_id%"
|
||||
withString:config.machineID];
|
||||
}
|
||||
if (hostname.length) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%hostname%" withString:hostname];
|
||||
}
|
||||
if (uuid.length) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%uuid%" withString:uuid];
|
||||
}
|
||||
if (serial.length) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%serial%" withString:serial];
|
||||
NSString *formatStr = url;
|
||||
if (!formatStr.length) {
|
||||
formatStr = config.eventDetailURL;
|
||||
if (!formatStr.length) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
return [NSURL URLWithString:formatStr];
|
||||
if ([formatStr isEqualToString:@"null"]) {
|
||||
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];
|
||||
|
||||
NSURL *u = [NSURL URLWithString:formatStr];
|
||||
if (!u) {
|
||||
LOGW(@"Unable to generate event detail URL for string '%@'", formatStr);
|
||||
}
|
||||
|
||||
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 *)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;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
95
Source/common/SNTBlockMessageTest.m
Normal file
95
Source/common/SNTBlockMessageTest.m
Normal file
@@ -0,0 +1,95 @@
|
||||
/// 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 <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#include "Source/common/SNTFileAccessEvent.h"
|
||||
#include "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/common/SNTSystemInfo.h"
|
||||
|
||||
@interface SNTBlockMessageTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@property id mockSystemInfo;
|
||||
@end
|
||||
|
||||
@implementation SNTBlockMessageTest
|
||||
|
||||
- (void)setUp {
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator machineID]).andReturn(@"my_mid");
|
||||
|
||||
self.mockSystemInfo = OCMClassMock([SNTSystemInfo class]);
|
||||
OCMStub([self.mockSystemInfo longHostname]).andReturn(@"my_hn");
|
||||
OCMStub([self.mockSystemInfo hardwareUUID]).andReturn(@"my_u");
|
||||
OCMStub([self.mockSystemInfo serialNumber]).andReturn(@"my_s");
|
||||
}
|
||||
|
||||
- (void)testEventDetailURLForEvent {
|
||||
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
|
||||
|
||||
se.fileSHA256 = @"my_fi";
|
||||
se.executingUser = @"my_un";
|
||||
|
||||
NSString *url = @"http://"
|
||||
@"localhost?fs=%file_sha%&fi=%file_identifier%&bfi=%bundle_or_file_identifier%&"
|
||||
@"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";
|
||||
|
||||
NSURL *gotUrl = [SNTBlockMessage eventDetailURLForEvent:se customURL:url];
|
||||
|
||||
// Set fileBundleHash and test again for newly expected values
|
||||
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";
|
||||
|
||||
gotUrl = [SNTBlockMessage eventDetailURLForEvent:se customURL:url];
|
||||
|
||||
XCTAssertEqualObjects(gotUrl.absoluteString, wantUrl);
|
||||
|
||||
XCTAssertNil([SNTBlockMessage eventDetailURLForEvent:se customURL:nil]);
|
||||
XCTAssertNil([SNTBlockMessage eventDetailURLForEvent:se customURL:@"null"]);
|
||||
}
|
||||
|
||||
- (void)testEventDetailURLForFileAccessEvent {
|
||||
SNTFileAccessEvent *fae = [[SNTFileAccessEvent alloc] init];
|
||||
|
||||
fae.ruleVersion = @"my_rv";
|
||||
fae.ruleName = @"my_rn";
|
||||
fae.fileSHA256 = @"my_fi";
|
||||
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 =
|
||||
@"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";
|
||||
|
||||
NSURL *gotUrl = [SNTBlockMessage eventDetailURLForFileAccessEvent:fae customURL:url];
|
||||
|
||||
XCTAssertEqualObjects(gotUrl.absoluteString, wantUrl);
|
||||
|
||||
XCTAssertNil([SNTBlockMessage eventDetailURLForFileAccessEvent:fae customURL:nil]);
|
||||
XCTAssertNil([SNTBlockMessage eventDetailURLForFileAccessEvent:fae customURL:@"null"]);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
@property SantaVnode vnodeId;
|
||||
@property SNTEventState decision;
|
||||
@property SNTClientMode decisionClientMode;
|
||||
@property NSString *decisionExtra;
|
||||
@property NSString *sha256;
|
||||
|
||||
@@ -36,10 +37,14 @@
|
||||
@property NSString *certCommonName;
|
||||
@property NSArray<MOLCertificate *> *certChain;
|
||||
@property NSString *teamID;
|
||||
@property NSString *signingID;
|
||||
@property NSDictionary *entitlements;
|
||||
@property BOOL entitlementsFiltered;
|
||||
|
||||
@property NSString *quarantineURL;
|
||||
|
||||
@property NSString *customMsg;
|
||||
@property NSString *customURL;
|
||||
@property BOOL silentBlock;
|
||||
|
||||
@end
|
||||
|
||||
@@ -36,12 +36,20 @@ typedef NS_ENUM(NSInteger, SNTAction) {
|
||||
#define RESPONSE_VALID(x) \
|
||||
(x == SNTActionRespondAllow || x == SNTActionRespondDeny || x == SNTActionRespondAllowCompiler)
|
||||
|
||||
// Supported Rule Types
|
||||
//
|
||||
// Note: These enum values should be in order of decreasing precedence as
|
||||
// evaluated by Santa. When adding new enum values, leave some space so that
|
||||
// additional rules can be added without violating this. The ordering isn't
|
||||
// strictly necessary but improves readability and may preemptively prevent
|
||||
// issues should SQLite behavior change.
|
||||
typedef NS_ENUM(NSInteger, SNTRuleType) {
|
||||
SNTRuleTypeUnknown,
|
||||
SNTRuleTypeUnknown = 0,
|
||||
|
||||
SNTRuleTypeBinary = 1,
|
||||
SNTRuleTypeCertificate = 2,
|
||||
SNTRuleTypeTeamID = 3,
|
||||
SNTRuleTypeBinary = 1000,
|
||||
SNTRuleTypeSigningID = 2000,
|
||||
SNTRuleTypeCertificate = 3000,
|
||||
SNTRuleTypeTeamID = 4000,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTRuleState) {
|
||||
@@ -63,32 +71,34 @@ typedef NS_ENUM(NSInteger, SNTClientMode) {
|
||||
SNTClientModeLockdown = 2,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTEventState) {
|
||||
typedef NS_ENUM(uint64_t, SNTEventState) {
|
||||
// Bits 0-15 bits store non-decision types
|
||||
SNTEventStateUnknown = 0,
|
||||
SNTEventStateBundleBinary = 1,
|
||||
|
||||
// Bits 16-23 store deny decision types
|
||||
SNTEventStateBlockUnknown = 1 << 16,
|
||||
SNTEventStateBlockBinary = 1 << 17,
|
||||
SNTEventStateBlockCertificate = 1 << 18,
|
||||
SNTEventStateBlockScope = 1 << 19,
|
||||
SNTEventStateBlockTeamID = 1 << 20,
|
||||
SNTEventStateBlockLongPath = 1 << 21,
|
||||
// Bits 16-39 store deny decision types
|
||||
SNTEventStateBlockUnknown = 1ULL << 16,
|
||||
SNTEventStateBlockBinary = 1ULL << 17,
|
||||
SNTEventStateBlockCertificate = 1ULL << 18,
|
||||
SNTEventStateBlockScope = 1ULL << 19,
|
||||
SNTEventStateBlockTeamID = 1ULL << 20,
|
||||
SNTEventStateBlockLongPath = 1ULL << 21,
|
||||
SNTEventStateBlockSigningID = 1ULL << 22,
|
||||
|
||||
// Bits 24-31 store allow decision types
|
||||
SNTEventStateAllowUnknown = 1 << 24,
|
||||
SNTEventStateAllowBinary = 1 << 25,
|
||||
SNTEventStateAllowCertificate = 1 << 26,
|
||||
SNTEventStateAllowScope = 1 << 27,
|
||||
SNTEventStateAllowCompiler = 1 << 28,
|
||||
SNTEventStateAllowTransitive = 1 << 29,
|
||||
SNTEventStateAllowPendingTransitive = 1 << 30,
|
||||
SNTEventStateAllowTeamID = 1 << 31,
|
||||
// Bits 40-63 store allow decision types
|
||||
SNTEventStateAllowUnknown = 1ULL << 40,
|
||||
SNTEventStateAllowBinary = 1ULL << 41,
|
||||
SNTEventStateAllowCertificate = 1ULL << 42,
|
||||
SNTEventStateAllowScope = 1ULL << 43,
|
||||
SNTEventStateAllowCompiler = 1ULL << 44,
|
||||
SNTEventStateAllowTransitive = 1ULL << 45,
|
||||
SNTEventStateAllowPendingTransitive = 1ULL << 46,
|
||||
SNTEventStateAllowTeamID = 1ULL << 47,
|
||||
SNTEventStateAllowSigningID = 1ULL << 48,
|
||||
|
||||
// Block and Allow masks
|
||||
SNTEventStateBlock = 0xFF << 16,
|
||||
SNTEventStateAllow = 0xFF << 24
|
||||
SNTEventStateBlock = 0xFFFFFFULL << 16,
|
||||
SNTEventStateAllow = 0xFFFFFFULL << 40,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTRuleTableError) {
|
||||
@@ -111,6 +121,7 @@ typedef NS_ENUM(NSInteger, SNTEventLogType) {
|
||||
SNTEventLogTypeSyslog,
|
||||
SNTEventLogTypeFilelog,
|
||||
SNTEventLogTypeProtobuf,
|
||||
SNTEventLogTypeJSON,
|
||||
SNTEventLogTypeNull,
|
||||
};
|
||||
|
||||
@@ -129,12 +140,44 @@ typedef NS_ENUM(NSInteger, SNTSyncStatusType) {
|
||||
SNTSyncStatusTypeUnknown,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTSyncContentEncoding) {
|
||||
SNTSyncContentEncodingNone,
|
||||
SNTSyncContentEncodingDeflate,
|
||||
SNTSyncContentEncodingGzip,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTMetricFormatType) {
|
||||
SNTMetricFormatTypeUnknown,
|
||||
SNTMetricFormatTypeRawJSON,
|
||||
SNTMetricFormatTypeMonarchJSON,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTOverrideFileAccessAction) {
|
||||
SNTOverrideFileAccessActionNone,
|
||||
SNTOverrideFileAccessActionAuditOnly,
|
||||
SNTOverrideFileAccessActionDiable,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTDeviceManagerStartupPreferences) {
|
||||
SNTDeviceManagerStartupPreferencesNone,
|
||||
SNTDeviceManagerStartupPreferencesUnmount,
|
||||
SNTDeviceManagerStartupPreferencesForceUnmount,
|
||||
SNTDeviceManagerStartupPreferencesRemount,
|
||||
SNTDeviceManagerStartupPreferencesForceRemount,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTSyncType) {
|
||||
SNTSyncTypeNormal,
|
||||
SNTSyncTypeClean,
|
||||
SNTSyncTypeCleanAll,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTRuleCleanup) {
|
||||
SNTRuleCleanupNone,
|
||||
SNTRuleCleanupAll,
|
||||
SNTRuleCleanupNonTransitive,
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
enum class FileAccessPolicyDecision {
|
||||
kNoPolicy,
|
||||
|
||||
@@ -245,13 +245,33 @@
|
||||
///
|
||||
@property(readonly, nonatomic) float spoolDirectoryEventMaxFlushTimeSec;
|
||||
|
||||
///
|
||||
/// If set, contains the filesystem access policy configuration.
|
||||
///
|
||||
/// @note: The property fileAccessPolicyPlist will be ignored if
|
||||
/// fileAccessPolicy is set.
|
||||
/// @note: This property is KVO compliant.
|
||||
///
|
||||
@property(readonly, nonatomic) NSDictionary *fileAccessPolicy;
|
||||
|
||||
///
|
||||
/// If set, contains the path to the filesystem access policy config plist.
|
||||
///
|
||||
/// @note: This property is KVO compliant, but is only read once at santad startup.
|
||||
/// @note: This property will be ignored if fileAccessPolicy is set.
|
||||
/// @note: This property is KVO compliant.
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *fileAccessPolicyPlist;
|
||||
|
||||
///
|
||||
/// This is the message shown to the user when access to a file is blocked
|
||||
/// by a binary due to some rule in the current File Access policy if that rule
|
||||
/// doesn't provide a custom message. If this is not configured, a reasonable
|
||||
/// default is provided.
|
||||
///
|
||||
/// @note: This property is KVO compliant.
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *fileAccessBlockMessage;
|
||||
|
||||
///
|
||||
/// If fileAccessPolicyPlist is set, fileAccessPolicyUpdateIntervalSec
|
||||
/// sets the number of seconds between times that the configuration file is
|
||||
@@ -264,7 +284,7 @@
|
||||
|
||||
///
|
||||
/// Enabling this appends the Santa machine ID to the end of each log line. If nothing
|
||||
/// has been overriden, this is the host's UUID.
|
||||
/// has been overridden, this is the host's UUID.
|
||||
/// Defaults to NO.
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableMachineIDDecoration;
|
||||
@@ -281,6 +301,14 @@
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableSilentMode;
|
||||
|
||||
///
|
||||
/// When silent TTY mode is enabled, Santa will not emit TTY notifications for
|
||||
/// blocked processes.
|
||||
///
|
||||
/// Defaults to NO.
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableSilentTTYMode;
|
||||
|
||||
///
|
||||
/// The text to display when opening Santa.app.
|
||||
/// If unset, the default text will be displayed.
|
||||
@@ -372,6 +400,27 @@
|
||||
///
|
||||
@property(readonly, nonatomic) NSDictionary *syncProxyConfig;
|
||||
|
||||
///
|
||||
/// Extra headers to include in all requests made during syncing.
|
||||
/// Keys and values must all be strings, any other type will be silently ignored.
|
||||
/// Some headers cannot be set through this key, including:
|
||||
///
|
||||
/// * Content-Encoding
|
||||
/// * Content-Length
|
||||
/// * Content-Type
|
||||
/// * Connection
|
||||
/// * Host
|
||||
/// * Proxy-Authenticate
|
||||
/// * Proxy-Authorization
|
||||
/// * WWW-Authenticate
|
||||
///
|
||||
/// The header "Authorization" is also documented by Apple to be one that will
|
||||
/// be ignored but this is not really the case, at least at present. If you
|
||||
/// are able to use a different header for this that would be safest but if not
|
||||
/// using Authorization /should/ be fine.
|
||||
///
|
||||
@property(readonly, nonatomic) NSDictionary *syncExtraHeaders;
|
||||
|
||||
///
|
||||
/// The machine owner.
|
||||
///
|
||||
@@ -388,9 +437,11 @@
|
||||
@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
|
||||
|
||||
///
|
||||
/// USB Mount Blocking. Defaults to false.
|
||||
@@ -398,11 +449,44 @@
|
||||
@property(nonatomic) BOOL blockUSBMount;
|
||||
|
||||
///
|
||||
/// Comma-seperated `$ mount -o` arguments used for forced remounting of USB devices. Default
|
||||
/// Comma-separated `$ mount -o` arguments used for forced remounting of USB devices. Default
|
||||
/// to fully allow/deny without remounting if unset.
|
||||
///
|
||||
@property(nonatomic) NSArray<NSString *> *remountUSBMode;
|
||||
|
||||
///
|
||||
/// If set, defines the action that should be taken on existing USB mounts when
|
||||
/// Santa starts up.
|
||||
///
|
||||
/// Supported values are:
|
||||
/// * "Unmount": Unmount mass storage devices
|
||||
/// * "ForceUnmount": Force unmount mass storage devices
|
||||
///
|
||||
///
|
||||
/// Note: Existing mounts with mount flags that are a superset of RemountUSBMode
|
||||
/// are unaffected and left mounted.
|
||||
///
|
||||
@property(readonly, nonatomic) SNTDeviceManagerStartupPreferences onStartUSBOptions;
|
||||
|
||||
///
|
||||
/// If set, will override the action taken when a file access rule violation
|
||||
/// occurs. This setting will apply across all rules in the file access policy.
|
||||
///
|
||||
/// Possible values are
|
||||
/// * "AuditOnly": When a rule is violated, it will be logged, but the access
|
||||
/// will not be blocked
|
||||
/// * "Disable": No access will be logged or blocked.
|
||||
///
|
||||
/// If not set, no override will take place and the file acces spolicy will
|
||||
/// apply as configured.
|
||||
///
|
||||
@property(readonly, nonatomic) SNTOverrideFileAccessAction overrideFileAccessAction;
|
||||
|
||||
///
|
||||
/// Set the action that will override file access policy config action
|
||||
///
|
||||
- (void)setSyncServerOverrideFileAccessAction:(NSString *)action;
|
||||
|
||||
///
|
||||
/// If set, this over-rides the default machine ID used for syncing.
|
||||
///
|
||||
@@ -502,6 +586,12 @@
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableBackwardsCompatibleContentEncoding;
|
||||
|
||||
///
|
||||
/// If set, "santactl sync" will use the supplied "Content-Encoding", possible
|
||||
/// settings include "gzip", "deflate", "none". If empty defaults to "deflate".
|
||||
///
|
||||
@property(readonly, nonatomic) SNTSyncContentEncoding syncClientContentEncoding;
|
||||
|
||||
///
|
||||
/// Contains the FCM project name.
|
||||
///
|
||||
@@ -552,6 +642,18 @@
|
||||
///
|
||||
@property(readonly, nonatomic) NSUInteger metricExportTimeout;
|
||||
|
||||
///
|
||||
/// List of prefix strings for which individual entitlement keys with a matching
|
||||
/// prefix should not be logged.
|
||||
///
|
||||
@property(readonly, nonatomic) NSArray<NSString *> *entitlementsPrefixFilter;
|
||||
|
||||
///
|
||||
/// List of TeamIDs for which entitlements should not be logged. Use the string
|
||||
/// "platform" to refer to platform binaries.
|
||||
///
|
||||
@property(readonly, nonatomic) NSArray<NSString *> *entitlementsTeamIDFilter;
|
||||
|
||||
///
|
||||
/// Retrieve an initialized singleton configurator object using the default file path.
|
||||
///
|
||||
|
||||
@@ -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
|
||||
@@ -56,6 +74,7 @@ static NSString *const kMobileConfigDomain = @"com.google.santa";
|
||||
static NSString *const kStaticRules = @"StaticRules";
|
||||
static NSString *const kSyncBaseURLKey = @"SyncBaseURL";
|
||||
static NSString *const kSyncProxyConfigKey = @"SyncProxyConfiguration";
|
||||
static NSString *const kSyncExtraHeadersKey = @"SyncExtraHeaders";
|
||||
static NSString *const kSyncEnableCleanSyncEventUpload = @"SyncEnableCleanSyncEventUpload";
|
||||
static NSString *const kClientAuthCertificateFileKey = @"ClientAuthCertificateFile";
|
||||
static NSString *const kClientAuthCertificatePasswordKey = @"ClientAuthCertificatePassword";
|
||||
@@ -72,6 +91,7 @@ static NSString *const kMachineIDPlistFileKey = @"MachineIDPlist";
|
||||
static NSString *const kMachineIDPlistKeyKey = @"MachineIDKey";
|
||||
|
||||
static NSString *const kEnableSilentModeKey = @"EnableSilentMode";
|
||||
static NSString *const kEnableSilentTTYModeKey = @"EnableSilentTTYMode";
|
||||
static NSString *const kAboutTextKey = @"AboutText";
|
||||
static NSString *const kMoreInfoURLKey = @"MoreInfoURL";
|
||||
static NSString *const kEventDetailURLKey = @"EventDetailURL";
|
||||
@@ -86,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";
|
||||
@@ -97,7 +119,9 @@ static NSString *const kSpoolDirectoryFileSizeThresholdKB = @"SpoolDirectoryFile
|
||||
static NSString *const kSpoolDirectorySizeThresholdMB = @"SpoolDirectorySizeThresholdMB";
|
||||
static NSString *const kSpoolDirectoryEventMaxFlushTimeSec = @"SpoolDirectoryEventMaxFlushTimeSec";
|
||||
|
||||
static NSString *const kFileAccessPolicy = @"FileAccessPolicy";
|
||||
static NSString *const kFileAccessPolicyPlist = @"FileAccessPolicyPlist";
|
||||
static NSString *const kFileAccessBlockMessage = @"FileAccessBlockMessage";
|
||||
static NSString *const kFileAccessPolicyUpdateIntervalSec = @"FileAccessPolicyUpdateIntervalSec";
|
||||
|
||||
static NSString *const kEnableMachineIDDecoration = @"EnableMachineIDDecoration";
|
||||
@@ -106,16 +130,25 @@ static NSString *const kEnableForkAndExitLogging = @"EnableForkAndExitLogging";
|
||||
static NSString *const kIgnoreOtherEndpointSecurityClients = @"IgnoreOtherEndpointSecurityClients";
|
||||
static NSString *const kEnableDebugLogging = @"EnableDebugLogging";
|
||||
|
||||
static NSString *const kEnableBackwardsCompatibleContentEncoding =
|
||||
@"EnableBackwardsCompatibleContentEncoding";
|
||||
static NSString *const kClientContentEncoding = @"SyncClientContentEncoding";
|
||||
|
||||
static NSString *const kFCMProject = @"FCMProject";
|
||||
static NSString *const kFCMEntity = @"FCMEntity";
|
||||
static NSString *const kFCMAPIKey = @"FCMAPIKey";
|
||||
|
||||
static NSString *const kEntitlementsPrefixFilterKey = @"EntitlementsPrefixFilter";
|
||||
static NSString *const kEntitlementsTeamIDFilterKey = @"EntitlementsTeamIDFilter";
|
||||
|
||||
static NSString *const kOnStartUSBOptions = @"OnStartUSBOptions";
|
||||
|
||||
static NSString *const kMetricFormat = @"MetricFormat";
|
||||
static NSString *const kMetricURL = @"MetricURL";
|
||||
static NSString *const kMetricExportInterval = @"MetricExportInterval";
|
||||
static NSString *const kMetricExportTimeout = @"MetricExportTimeout";
|
||||
static NSString *const kMetricExtraLabels = @"MetricExtraLabels";
|
||||
|
||||
// The keys managed by a sync server or mobileconfig.
|
||||
static NSString *const kClientModeKey = @"ClientMode";
|
||||
static NSString *const kFailClosedKey = @"FailClosed";
|
||||
static NSString *const kBlockUSBMountKey = @"BlockUSBMount";
|
||||
static NSString *const kRemountUSBModeKey = @"RemountUSBMode";
|
||||
static NSString *const kEnableTransitiveRulesKey = @"EnableTransitiveRules";
|
||||
@@ -125,21 +158,24 @@ static NSString *const kAllowedPathRegexKeyDeprecated = @"WhitelistRegex";
|
||||
static NSString *const kBlockedPathRegexKey = @"BlockedPathRegex";
|
||||
static NSString *const kBlockedPathRegexKeyDeprecated = @"BlacklistRegex";
|
||||
static NSString *const kEnableAllEventUploadKey = @"EnableAllEventUpload";
|
||||
static NSString *const kDisableUnknownEventUploadKey = @"DisableUnknownEventUpload";
|
||||
|
||||
// TODO(markowsky): move these to sync server only.
|
||||
static NSString *const kMetricFormat = @"MetricFormat";
|
||||
static NSString *const kMetricURL = @"MetricURL";
|
||||
static NSString *const kMetricExportInterval = @"MetricExportInterval";
|
||||
static NSString *const kMetricExportTimeout = @"MetricExportTimeout";
|
||||
static NSString *const kMetricExtraLabels = @"MetricExtraLabels";
|
||||
static NSString *const 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];
|
||||
@@ -161,8 +197,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kRemountUSBModeKey : array,
|
||||
kFullSyncLastSuccess : date,
|
||||
kRuleSyncLastSuccess : date,
|
||||
kSyncCleanRequired : number,
|
||||
kSyncCleanRequiredDeprecated : number,
|
||||
kSyncTypeRequired : number,
|
||||
kEnableAllEventUploadKey : number,
|
||||
kOverrideFileAccessActionKey : string,
|
||||
};
|
||||
_forcedConfigKeyTypes = @{
|
||||
kClientModeKey : number,
|
||||
@@ -177,9 +215,11 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kBlockedPathRegexKeyDeprecated : re,
|
||||
kBlockUSBMountKey : number,
|
||||
kRemountUSBModeKey : array,
|
||||
kOnStartUSBOptions : string,
|
||||
kEnablePageZeroProtectionKey : number,
|
||||
kEnableBadSignatureProtectionKey : number,
|
||||
kEnableSilentModeKey : number,
|
||||
kEnableSilentTTYModeKey : number,
|
||||
kAboutTextKey : string,
|
||||
kMoreInfoURLKey : string,
|
||||
kEventDetailURLKey : string,
|
||||
@@ -192,11 +232,14 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kModeNotificationLockdown : string,
|
||||
kStaticRules : array,
|
||||
kSyncBaseURLKey : string,
|
||||
kSyncEnableCleanSyncEventUpload : number,
|
||||
kSyncProxyConfigKey : dictionary,
|
||||
kSyncExtraHeadersKey : dictionary,
|
||||
kClientAuthCertificateFileKey : string,
|
||||
kClientAuthCertificatePasswordKey : string,
|
||||
kClientAuthCertificateCNKey : string,
|
||||
kClientAuthCertificateIssuerKey : string,
|
||||
kClientContentEncoding : string,
|
||||
kServerAuthRootsDataKey : data,
|
||||
kServerAuthRootsFileKey : string,
|
||||
kMachineOwnerKey : string,
|
||||
@@ -211,13 +254,14 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kSpoolDirectoryFileSizeThresholdKB : number,
|
||||
kSpoolDirectorySizeThresholdMB : number,
|
||||
kSpoolDirectoryEventMaxFlushTimeSec : number,
|
||||
kFileAccessPolicy : dictionary,
|
||||
kFileAccessPolicyPlist : string,
|
||||
kFileAccessBlockMessage : string,
|
||||
kFileAccessPolicyUpdateIntervalSec : number,
|
||||
kEnableMachineIDDecoration : number,
|
||||
kEnableForkAndExitLogging : number,
|
||||
kIgnoreOtherEndpointSecurityClients : number,
|
||||
kEnableDebugLogging : number,
|
||||
kEnableBackwardsCompatibleContentEncoding : number,
|
||||
kFCMProject : string,
|
||||
kFCMEntity : string,
|
||||
kFCMAPIKey : string,
|
||||
@@ -228,12 +272,25 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kMetricExtraLabels : dictionary,
|
||||
kEnableAllEventUploadKey : number,
|
||||
kDisableUnknownEventUploadKey : number,
|
||||
kOverrideFileAccessActionKey : string,
|
||||
kEntitlementsPrefixFilterKey : array,
|
||||
kEntitlementsTeamIDFilterKey : array,
|
||||
};
|
||||
|
||||
_syncStateFilePath = syncStateFilePath;
|
||||
_syncStateAccessAuthorizerBlock = syncStateAccessAuthorizer;
|
||||
|
||||
_defaults = [NSUserDefaults standardUserDefaults];
|
||||
[_defaults addSuiteNamed:@"com.google.santa"];
|
||||
_configState = [self readForcedConfig];
|
||||
[self cacheStaticRules];
|
||||
|
||||
_syncState = [self readSyncStateFromDisk] ?: [NSMutableDictionary dictionary];
|
||||
if ([self migrateDeprecatedSyncStateKeys]) {
|
||||
// Save the updated sync state if any keys were migrated.
|
||||
[self saveSyncStateToDisk];
|
||||
}
|
||||
|
||||
_debugFlag = [[NSProcessInfo processInfo].arguments containsObject:@"--debug"];
|
||||
[self startWatchingDefaults];
|
||||
}
|
||||
@@ -311,6 +368,14 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingSyncExtraHeaders {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableCleanSyncEventUpload {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnablePageZeroProtection {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -391,7 +456,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self syncStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingSyncCleanRequired {
|
||||
+ (NSSet *)keyPathsForValuesAffectingSyncTypeRequired {
|
||||
return [self syncStateSet];
|
||||
}
|
||||
|
||||
@@ -419,10 +484,18 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicy {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicyPlist {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFileAccessBlockMessage {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicyUpdateIntervalSec {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -455,10 +528,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableBackwardsCompatibleContentEncoding {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFcmProject {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -499,6 +568,18 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingOverrideFileAccessActionKey {
|
||||
return [self syncAndConfigStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEntitlementsPrefixFilter {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEntitlementsTeamIDFilter {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
#pragma mark Public Interface
|
||||
|
||||
- (SNTClientMode)clientMode {
|
||||
@@ -609,6 +690,22 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return args;
|
||||
}
|
||||
|
||||
- (SNTDeviceManagerStartupPreferences)onStartUSBOptions {
|
||||
NSString *action = [self.configState[kOnStartUSBOptions] lowercaseString];
|
||||
|
||||
if ([action isEqualToString:@"unmount"]) {
|
||||
return SNTDeviceManagerStartupPreferencesUnmount;
|
||||
} else if ([action isEqualToString:@"forceunmount"]) {
|
||||
return SNTDeviceManagerStartupPreferencesForceUnmount;
|
||||
} else if ([action isEqualToString:@"remount"]) {
|
||||
return SNTDeviceManagerStartupPreferencesRemount;
|
||||
} else if ([action isEqualToString:@"forceremount"]) {
|
||||
return SNTDeviceManagerStartupPreferencesForceRemount;
|
||||
} else {
|
||||
return SNTDeviceManagerStartupPreferencesNone;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *, SNTRule *> *)staticRules {
|
||||
return self.cachedStaticRules;
|
||||
}
|
||||
@@ -624,6 +721,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return self.configState[kSyncProxyConfigKey];
|
||||
}
|
||||
|
||||
- (NSDictionary *)syncExtraHeaders {
|
||||
return self.configState[kSyncExtraHeadersKey];
|
||||
}
|
||||
|
||||
- (BOOL)enablePageZeroProtection {
|
||||
NSNumber *number = self.configState[kEnablePageZeroProtectionKey];
|
||||
return number ? [number boolValue] : YES;
|
||||
@@ -639,6 +740,11 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (BOOL)enableSilentTTYMode {
|
||||
NSNumber *number = self.configState[kEnableSilentTTYModeKey];
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (NSString *)aboutText {
|
||||
return self.configState[kAboutTextKey];
|
||||
}
|
||||
@@ -702,6 +808,20 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return self.configState[kClientAuthCertificateIssuerKey];
|
||||
}
|
||||
|
||||
- (SNTSyncContentEncoding)syncClientContentEncoding {
|
||||
NSString *contentEncoding = [self.configState[kClientContentEncoding] lowercaseString];
|
||||
if ([contentEncoding isEqualToString:@"deflate"]) {
|
||||
return SNTSyncContentEncodingDeflate;
|
||||
} else if ([contentEncoding isEqualToString:@"gzip"]) {
|
||||
return SNTSyncContentEncodingGzip;
|
||||
} else if ([contentEncoding isEqualToString:@"none"]) {
|
||||
return SNTSyncContentEncodingNone;
|
||||
} else {
|
||||
// Ensure we have the same default zlib behavior Santa's always had otherwise.
|
||||
return SNTSyncContentEncodingDeflate;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSData *)syncServerAuthRootsData {
|
||||
return self.configState[kServerAuthRootsDataKey];
|
||||
}
|
||||
@@ -727,12 +847,12 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
[self updateSyncStateForKey:kRuleSyncLastSuccess value:ruleSyncLastSuccess];
|
||||
}
|
||||
|
||||
- (BOOL)syncCleanRequired {
|
||||
return [self.syncState[kSyncCleanRequired] boolValue];
|
||||
- (SNTSyncType)syncTypeRequired {
|
||||
return (SNTSyncType)[self.syncState[kSyncTypeRequired] integerValue];
|
||||
}
|
||||
|
||||
- (void)setSyncCleanRequired:(BOOL)syncCleanRequired {
|
||||
[self updateSyncStateForKey:kSyncCleanRequired value:@(syncCleanRequired)];
|
||||
- (void)setSyncTypeRequired:(SNTSyncType)syncTypeRequired {
|
||||
[self updateSyncStateForKey:kSyncTypeRequired value:@(syncTypeRequired)];
|
||||
}
|
||||
|
||||
- (NSString *)machineOwner {
|
||||
@@ -772,6 +892,8 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return SNTEventLogTypeSyslog;
|
||||
} else if ([logType isEqualToString:@"null"]) {
|
||||
return SNTEventLogTypeNull;
|
||||
} else if ([logType isEqualToString:@"json"]) {
|
||||
return SNTEventLogTypeJSON;
|
||||
} else if ([logType isEqualToString:@"file"]) {
|
||||
return SNTEventLogTypeFilelog;
|
||||
} else {
|
||||
@@ -809,8 +931,21 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
: 15.0;
|
||||
}
|
||||
|
||||
- (NSDictionary *)fileAccessPolicy {
|
||||
return self.configState[kFileAccessPolicy];
|
||||
}
|
||||
|
||||
- (NSString *)fileAccessPolicyPlist {
|
||||
return self.configState[kFileAccessPolicyPlist];
|
||||
// This property is ignored when kFileAccessPolicy is set
|
||||
if (self.configState[kFileAccessPolicy]) {
|
||||
return nil;
|
||||
} else {
|
||||
return self.configState[kFileAccessPolicyPlist];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)fileAccessBlockMessage {
|
||||
return self.configState[kFileAccessBlockMessage];
|
||||
}
|
||||
|
||||
- (uint32_t)fileAccessPolicyUpdateIntervalSec {
|
||||
@@ -866,11 +1001,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [number boolValue] || self.debugFlag;
|
||||
}
|
||||
|
||||
- (BOOL)enableBackwardsCompatibleContentEncoding {
|
||||
NSNumber *number = self.configState[kEnableBackwardsCompatibleContentEncoding];
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (NSString *)fcmProject {
|
||||
return self.configState[kFCMProject];
|
||||
}
|
||||
@@ -898,6 +1028,33 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self.configState[kBlockUSBMountKey] boolValue];
|
||||
}
|
||||
|
||||
- (void)setSyncServerOverrideFileAccessAction:(NSString *)action {
|
||||
NSString *a = [action lowercaseString];
|
||||
if ([a isEqualToString:@"auditonly"] || [a isEqualToString:@"disable"] ||
|
||||
[a isEqualToString:@"none"] || [a isEqualToString:@""]) {
|
||||
[self updateSyncStateForKey:kOverrideFileAccessActionKey value:action];
|
||||
}
|
||||
}
|
||||
|
||||
- (SNTOverrideFileAccessAction)overrideFileAccessAction {
|
||||
NSString *action = [self.syncState[kOverrideFileAccessActionKey] lowercaseString];
|
||||
|
||||
if (!action) {
|
||||
action = [self.configState[kOverrideFileAccessActionKey] lowercaseString];
|
||||
if (!action) {
|
||||
return SNTOverrideFileAccessActionNone;
|
||||
}
|
||||
}
|
||||
|
||||
if ([action isEqualToString:@"auditonly"]) {
|
||||
return SNTOverrideFileAccessActionAuditOnly;
|
||||
} else if ([action isEqualToString:@"disable"]) {
|
||||
return SNTOverrideFileAccessActionDiable;
|
||||
} else {
|
||||
return SNTOverrideFileAccessActionNone;
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns YES if all of the necessary options are set to export metrics, NO
|
||||
/// otherwise.
|
||||
@@ -967,12 +1124,12 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
/// Read the saved syncState.
|
||||
///
|
||||
- (NSMutableDictionary *)readSyncStateFromDisk {
|
||||
// Only read the sync state if a sync server is configured.
|
||||
if (!self.syncBaseURL) return nil;
|
||||
// Only santad should read this file.
|
||||
if (geteuid() != 0) return nil;
|
||||
if (!self.syncStateAccessAuthorizerBlock()) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableDictionary *syncState =
|
||||
[NSMutableDictionary dictionaryWithContentsOfFile:kSyncStateFilePath];
|
||||
[NSMutableDictionary dictionaryWithContentsOfFile:self.syncStateFilePath];
|
||||
for (NSString *key in syncState.allKeys) {
|
||||
if (self.syncServerKeyTypes[key] == [NSRegularExpression class]) {
|
||||
NSString *pattern = [syncState[key] isKindOfClass:[NSString class]] ? syncState[key] : nil;
|
||||
@@ -982,24 +1139,54 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return syncState;
|
||||
}
|
||||
|
||||
///
|
||||
/// Migrate any deprecated sync state keys/values to alternative keys/values.
|
||||
///
|
||||
/// Returns YES if any keys were migrated. Otherwise NO.
|
||||
///
|
||||
- (BOOL)migrateDeprecatedSyncStateKeys {
|
||||
// Currently only one key to migrate
|
||||
if (!self.syncState[kSyncCleanRequiredDeprecated]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSMutableDictionary *syncState = self.syncState.mutableCopy;
|
||||
|
||||
// If the kSyncTypeRequired key exists, its current value will take precedence.
|
||||
// Otherwise, migrate the old value to be compatible with the new logic.
|
||||
if (!self.syncState[kSyncTypeRequired]) {
|
||||
syncState[kSyncTypeRequired] = [self.syncState[kSyncCleanRequiredDeprecated] boolValue]
|
||||
? @(SNTSyncTypeClean)
|
||||
: @(SNTSyncTypeNormal);
|
||||
}
|
||||
|
||||
// Delete the deprecated key
|
||||
syncState[kSyncCleanRequiredDeprecated] = nil;
|
||||
|
||||
self.syncState = syncState;
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
///
|
||||
/// Saves the current effective syncState to disk.
|
||||
///
|
||||
- (void)saveSyncStateToDisk {
|
||||
// Only save the sync state if a sync server is configured.
|
||||
if (!self.syncBaseURL) return;
|
||||
// Only santad should write to this file.
|
||||
if (geteuid() != 0) return;
|
||||
if (!self.syncStateAccessAuthorizerBlock()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Either remove
|
||||
NSMutableDictionary *syncState = self.syncState.mutableCopy;
|
||||
syncState[kAllowedPathRegexKey] = [syncState[kAllowedPathRegexKey] pattern];
|
||||
syncState[kBlockedPathRegexKey] = [syncState[kBlockedPathRegexKey] pattern];
|
||||
[syncState writeToFile:kSyncStateFilePath atomically:YES];
|
||||
[syncState writeToFile:self.syncStateFilePath atomically:YES];
|
||||
[[NSFileManager defaultManager] setAttributes:@{NSFilePosixPermissions : @0600}
|
||||
ofItemAtPath:kSyncStateFilePath
|
||||
ofItemAtPath:self.syncStateFilePath
|
||||
error:NULL];
|
||||
}
|
||||
|
||||
@@ -1007,6 +1194,14 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
self.syncState = [NSMutableDictionary dictionary];
|
||||
}
|
||||
|
||||
- (NSArray *)entitlementsPrefixFilter {
|
||||
return EnsureArrayOfStrings(self.configState[kEntitlementsPrefixFilterKey]);
|
||||
}
|
||||
|
||||
- (NSArray *)entitlementsTeamIDFilter {
|
||||
return EnsureArrayOfStrings(self.configState[kEntitlementsTeamIDFilterKey]);
|
||||
}
|
||||
|
||||
#pragma mark Private Defaults Methods
|
||||
|
||||
- (NSRegularExpression *)expressionForPattern:(NSString *)pattern {
|
||||
@@ -1030,7 +1225,11 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
NSDictionary *overrides = [NSDictionary dictionaryWithContentsOfFile:kConfigOverrideFilePath];
|
||||
for (NSString *key in overrides) {
|
||||
id obj = overrides[key];
|
||||
if (![obj isKindOfClass:self.forcedConfigKeyTypes[key]]) continue;
|
||||
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;
|
||||
@@ -1120,6 +1319,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
for (id rule in staticRules) {
|
||||
if (![rule isKindOfClass:[NSDictionary class]]) return;
|
||||
SNTRule *r = [[SNTRule alloc] initWithDictionary:rule];
|
||||
if (!r) continue;
|
||||
rules[r.identifier] = r;
|
||||
}
|
||||
self.cachedStaticRules = [rules copy];
|
||||
|
||||
102
Source/common/SNTConfiguratorTest.m
Normal file
102
Source/common/SNTConfiguratorTest.m
Normal 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
|
||||
27
Source/common/SNTDeepCopy.h
Normal file
27
Source/common/SNTDeepCopy.h
Normal 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
|
||||
53
Source/common/SNTDeepCopy.m
Normal file
53
Source/common/SNTDeepCopy.m
Normal 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
|
||||
99
Source/common/SNTFileAccessEvent.h
Normal file
99
Source/common/SNTFileAccessEvent.h
Normal file
@@ -0,0 +1,99 @@
|
||||
/// 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>
|
||||
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
|
||||
///
|
||||
/// Represents an event stored in the database.
|
||||
///
|
||||
@interface SNTFileAccessEvent : NSObject <NSSecureCoding>
|
||||
|
||||
///
|
||||
/// The watched path that was accessed
|
||||
///
|
||||
@property NSString *accessedPath;
|
||||
|
||||
///
|
||||
/// The rule version and name that were violated
|
||||
///
|
||||
@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
|
||||
///
|
||||
@property(readonly) NSString *publisherInfo;
|
||||
|
||||
///
|
||||
/// Return an array of the underlying SecCertificateRef's of the signingChain
|
||||
///
|
||||
/// WARNING: If the refs need to be used for a long time be careful to properly
|
||||
/// CFRetain/CFRelease the returned items.
|
||||
///
|
||||
@property(readonly) NSArray *signingChainCertRefs;
|
||||
|
||||
@end
|
||||
97
Source/common/SNTFileAccessEvent.m
Normal file
97
Source/common/SNTFileAccessEvent.m
Normal file
@@ -0,0 +1,97 @@
|
||||
/// 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/SNTFileAccessEvent.h"
|
||||
|
||||
#import "Source/common/CertificateHelpers.h"
|
||||
|
||||
@implementation SNTFileAccessEvent
|
||||
|
||||
#define ENCODE(o) \
|
||||
do { \
|
||||
if (self.o) { \
|
||||
[coder encodeObject:self.o forKey:@(#o)]; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define DECODE(o, c) \
|
||||
do { \
|
||||
_##o = [decoder decodeObjectOfClass:[c class] forKey:@(#o)]; \
|
||||
} while (0)
|
||||
|
||||
#define DECODEARRAY(o, c) \
|
||||
do { \
|
||||
_##o = [decoder decodeObjectOfClasses:[NSSet setWithObjects:[NSArray class], [c class], nil] \
|
||||
forKey:@(#o)]; \
|
||||
} while (0)
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)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];
|
||||
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;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString
|
||||
stringWithFormat:@"SNTFileAccessEvent: Accessed: %@, By: %@", self.accessedPath, self.filePath];
|
||||
}
|
||||
|
||||
- (NSString *)publisherInfo {
|
||||
return Publisher(self.signingChain, self.teamID);
|
||||
}
|
||||
|
||||
- (NSArray *)signingChainCertRefs {
|
||||
return CertificateChain(self.signingChain);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -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
|
||||
|
||||
@@ -41,6 +41,11 @@
|
||||
///
|
||||
@property(copy) NSString *customMsg;
|
||||
|
||||
///
|
||||
/// A custom URL to take the user to when this binary is blocked from executing.
|
||||
///
|
||||
@property(copy) NSString *customURL;
|
||||
|
||||
///
|
||||
/// The time when this rule was last retrieved from the rules database, if rule is transitive.
|
||||
/// Stored as number of seconds since 00:00:00 UTC on 1 January 2001.
|
||||
@@ -74,4 +79,9 @@
|
||||
///
|
||||
- (void)resetTimestamp;
|
||||
|
||||
///
|
||||
/// Returns a dictionary representation of the rule.
|
||||
///
|
||||
- (NSDictionary *)dictionaryRepresentation;
|
||||
|
||||
@end
|
||||
|
||||
@@ -13,8 +13,15 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTRule.h"
|
||||
|
||||
#include <CommonCrypto/CommonCrypto.h>
|
||||
#include <os/base.h>
|
||||
|
||||
#import "Source/common/SNTSyncConstants.h"
|
||||
|
||||
// https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/
|
||||
static const NSUInteger kExpectedTeamIDLength = 10;
|
||||
|
||||
@interface SNTRule ()
|
||||
@property(readwrite) NSUInteger timestamp;
|
||||
@end
|
||||
@@ -28,6 +35,79 @@
|
||||
timestamp:(NSUInteger)timestamp {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
if (identifier.length == 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSCharacterSet *nonHex =
|
||||
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdef"] invertedSet];
|
||||
NSCharacterSet *nonUppercaseAlphaNumeric = [[NSCharacterSet
|
||||
characterSetWithCharactersInString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"] invertedSet];
|
||||
|
||||
switch (type) {
|
||||
case SNTRuleTypeBinary: OS_FALLTHROUGH;
|
||||
case SNTRuleTypeCertificate: {
|
||||
// For binary and certificate rules, force the hash identifier to be lowercase hex.
|
||||
identifier = [identifier lowercaseString];
|
||||
|
||||
identifier = [identifier stringByTrimmingCharactersInSet:nonHex];
|
||||
if (identifier.length != (CC_SHA256_DIGEST_LENGTH * 2)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SNTRuleTypeTeamID: {
|
||||
// TeamIDs are always [0-9A-Z], so enforce that the identifier is uppercase
|
||||
identifier =
|
||||
[[identifier uppercaseString] stringByTrimmingCharactersInSet:nonUppercaseAlphaNumeric];
|
||||
if (identifier.length != kExpectedTeamIDLength) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SNTRuleTypeSigningID: {
|
||||
// SigningID rules are a combination of `TeamID:SigningID`. The TeamID should
|
||||
// be forced to be uppercase, but because very loose rules exist for SigningIDs,
|
||||
// their case will be kept as-is. However, platform binaries are expected to
|
||||
// have the hardcoded string "platform" as the team ID and the case will be left
|
||||
// as is.
|
||||
NSArray *sidComponents = [identifier componentsSeparatedByString:@":"];
|
||||
if (!sidComponents || sidComponents.count < 2) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// The first component is the TeamID
|
||||
NSString *teamID = sidComponents[0];
|
||||
|
||||
if (![teamID isEqualToString:@"platform"]) {
|
||||
teamID =
|
||||
[[teamID uppercaseString] stringByTrimmingCharactersInSet:nonUppercaseAlphaNumeric];
|
||||
if (teamID.length != kExpectedTeamIDLength) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
// The rest of the components are the Signing ID since ":" a legal character.
|
||||
// Join all but the last element of the components to rebuild the SigningID.
|
||||
NSString *signingID = [[sidComponents
|
||||
subarrayWithRange:NSMakeRange(1, sidComponents.count - 1)] componentsJoinedByString:@":"];
|
||||
if (signingID.length == 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
identifier = [NSString stringWithFormat:@"%@:%@", teamID, signingID];
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_identifier = identifier;
|
||||
_state = state;
|
||||
_type = type;
|
||||
@@ -55,52 +135,61 @@
|
||||
- (instancetype)initWithDictionary:(NSDictionary *)dict {
|
||||
if (![dict isKindOfClass:[NSDictionary class]]) return nil;
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_identifier = dict[kRuleIdentifier];
|
||||
if (![_identifier isKindOfClass:[NSString class]] || !_identifier.length) {
|
||||
_identifier = dict[kRuleSHA256];
|
||||
}
|
||||
if (![_identifier isKindOfClass:[NSString class]] || !_identifier.length) return nil;
|
||||
|
||||
NSString *policyString = dict[kRulePolicy];
|
||||
if (![policyString isKindOfClass:[NSString class]]) return nil;
|
||||
if ([policyString isEqual:kRulePolicyAllowlist] ||
|
||||
[policyString isEqual:kRulePolicyAllowlistDeprecated]) {
|
||||
_state = SNTRuleStateAllow;
|
||||
} else if ([policyString isEqual:kRulePolicyAllowlistCompiler] ||
|
||||
[policyString isEqual:kRulePolicyAllowlistCompilerDeprecated]) {
|
||||
_state = SNTRuleStateAllowCompiler;
|
||||
} else if ([policyString isEqual:kRulePolicyBlocklist] ||
|
||||
[policyString isEqual:kRulePolicyBlocklistDeprecated]) {
|
||||
_state = SNTRuleStateBlock;
|
||||
} else if ([policyString isEqual:kRulePolicySilentBlocklist] ||
|
||||
[policyString isEqual:kRulePolicySilentBlocklistDeprecated]) {
|
||||
_state = SNTRuleStateSilentBlock;
|
||||
} else if ([policyString isEqual:kRulePolicyRemove]) {
|
||||
_state = SNTRuleStateRemove;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *ruleTypeString = dict[kRuleType];
|
||||
if (![ruleTypeString isKindOfClass:[NSString class]]) return nil;
|
||||
if ([ruleTypeString isEqual:kRuleTypeBinary]) {
|
||||
_type = SNTRuleTypeBinary;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeCertificate]) {
|
||||
_type = SNTRuleTypeCertificate;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeTeamID]) {
|
||||
_type = SNTRuleTypeTeamID;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *customMsg = dict[kRuleCustomMsg];
|
||||
if ([customMsg isKindOfClass:[NSString class]] && customMsg.length) {
|
||||
_customMsg = customMsg;
|
||||
}
|
||||
NSString *identifier = dict[kRuleIdentifier];
|
||||
if (![identifier isKindOfClass:[NSString class]] || !identifier.length) {
|
||||
identifier = dict[kRuleSHA256];
|
||||
}
|
||||
return self;
|
||||
if (![identifier isKindOfClass:[NSString class]] || !identifier.length) return nil;
|
||||
|
||||
NSString *policyString = dict[kRulePolicy];
|
||||
SNTRuleState state;
|
||||
if (![policyString isKindOfClass:[NSString class]]) return nil;
|
||||
if ([policyString isEqual:kRulePolicyAllowlist] ||
|
||||
[policyString isEqual:kRulePolicyAllowlistDeprecated]) {
|
||||
state = SNTRuleStateAllow;
|
||||
} else if ([policyString isEqual:kRulePolicyAllowlistCompiler] ||
|
||||
[policyString isEqual:kRulePolicyAllowlistCompilerDeprecated]) {
|
||||
state = SNTRuleStateAllowCompiler;
|
||||
} else if ([policyString isEqual:kRulePolicyBlocklist] ||
|
||||
[policyString isEqual:kRulePolicyBlocklistDeprecated]) {
|
||||
state = SNTRuleStateBlock;
|
||||
} else if ([policyString isEqual:kRulePolicySilentBlocklist] ||
|
||||
[policyString isEqual:kRulePolicySilentBlocklistDeprecated]) {
|
||||
state = SNTRuleStateSilentBlock;
|
||||
} else if ([policyString isEqual:kRulePolicyRemove]) {
|
||||
state = SNTRuleStateRemove;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *ruleTypeString = dict[kRuleType];
|
||||
SNTRuleType type;
|
||||
if (![ruleTypeString isKindOfClass:[NSString class]]) return nil;
|
||||
if ([ruleTypeString isEqual:kRuleTypeBinary]) {
|
||||
type = SNTRuleTypeBinary;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeCertificate]) {
|
||||
type = SNTRuleTypeCertificate;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeTeamID]) {
|
||||
type = SNTRuleTypeTeamID;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeSigningID]) {
|
||||
type = SNTRuleTypeSigningID;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *customMsg = dict[kRuleCustomMsg];
|
||||
if (![customMsg isKindOfClass:[NSString class]] || customMsg.length == 0) {
|
||||
customMsg = nil;
|
||||
}
|
||||
|
||||
NSString *customURL = dict[kRuleCustomURL];
|
||||
if (![customURL isKindOfClass:[NSString class]] || customURL.length == 0) {
|
||||
customURL = nil;
|
||||
}
|
||||
|
||||
SNTRule *r = [self initWithIdentifier:identifier state:state type:type customMsg:customMsg];
|
||||
r.customURL = customURL;
|
||||
return r;
|
||||
}
|
||||
|
||||
#pragma mark NSSecureCoding
|
||||
@@ -120,6 +209,7 @@
|
||||
ENCODE(@(self.state), @"state");
|
||||
ENCODE(@(self.type), @"type");
|
||||
ENCODE(self.customMsg, @"custommsg");
|
||||
ENCODE(self.customURL, @"customurl");
|
||||
ENCODE(@(self.timestamp), @"timestamp");
|
||||
}
|
||||
|
||||
@@ -130,11 +220,49 @@
|
||||
_state = [DECODE(NSNumber, @"state") intValue];
|
||||
_type = [DECODE(NSNumber, @"type") intValue];
|
||||
_customMsg = DECODE(NSString, @"custommsg");
|
||||
_customURL = DECODE(NSString, @"customurl");
|
||||
_timestamp = [DECODE(NSNumber, @"timestamp") unsignedIntegerValue];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)ruleStateToPolicyString:(SNTRuleState)state {
|
||||
switch (state) {
|
||||
case SNTRuleStateAllow: return kRulePolicyAllowlist;
|
||||
case SNTRuleStateAllowCompiler: return kRulePolicyAllowlistCompiler;
|
||||
case SNTRuleStateBlock: return kRulePolicyBlocklist;
|
||||
case SNTRuleStateSilentBlock: return kRulePolicySilentBlocklist;
|
||||
case SNTRuleStateRemove: return kRulePolicyRemove;
|
||||
case SNTRuleStateAllowTransitive: return @"AllowTransitive";
|
||||
// This should never be hit. But is here for completion.
|
||||
default: return @"Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)ruleTypeToString:(SNTRuleType)ruleType {
|
||||
switch (ruleType) {
|
||||
case SNTRuleTypeBinary: return kRuleTypeBinary;
|
||||
case SNTRuleTypeCertificate: return kRuleTypeCertificate;
|
||||
case SNTRuleTypeTeamID: return kRuleTypeTeamID;
|
||||
case SNTRuleTypeSigningID: return kRuleTypeSigningID;
|
||||
// This should never be hit. If we have rule types of Unknown then there's a
|
||||
// coding error somewhere.
|
||||
default: return @"Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an NSDictionary representation of the rule. Primarily use for
|
||||
// exporting rules.
|
||||
- (NSDictionary *)dictionaryRepresentation {
|
||||
return @{
|
||||
kRuleIdentifier : self.identifier,
|
||||
kRulePolicy : [self ruleStateToPolicyString:self.state],
|
||||
kRuleType : [self ruleTypeToString:self.type],
|
||||
kRuleCustomMsg : self.customMsg ?: @"",
|
||||
kRuleCustomURL : self.customURL ?: @""
|
||||
};
|
||||
}
|
||||
|
||||
#undef DECODE
|
||||
#undef ENCODE
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTSyncConstants.h"
|
||||
|
||||
#import "Source/common/SNTRule.h"
|
||||
|
||||
@interface SNTRuleTest : XCTestCase
|
||||
@@ -25,66 +28,169 @@
|
||||
SNTRule *sut;
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"some-sort-of-identifier",
|
||||
@"identifier" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
@"policy" : @"ALLOWLIST",
|
||||
@"rule_type" : @"BINARY",
|
||||
}];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
|
||||
XCTAssertEqualObjects(sut.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(sut.type, SNTRuleTypeBinary);
|
||||
XCTAssertEqual(sut.state, SNTRuleStateAllow);
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"sha256" : @"some-sort-of-identifier",
|
||||
@"sha256" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
@"policy" : @"BLOCKLIST",
|
||||
@"rule_type" : @"CERTIFICATE",
|
||||
}];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
|
||||
XCTAssertEqualObjects(sut.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(sut.type, SNTRuleTypeCertificate);
|
||||
XCTAssertEqual(sut.state, SNTRuleStateBlock);
|
||||
|
||||
// Ensure a Binary and Certificate rules properly convert identifiers to lowercase.
|
||||
for (NSString *ruleType in @[ @"BINARY", @"CERTIFICATE" ]) {
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"B7C1E3FD640C5F211C89B02C2C6122F78CE322AA5C56EB0BB54BC422A8F8B670",
|
||||
@"policy" : @"BLOCKLIST",
|
||||
@"rule_type" : ruleType,
|
||||
}];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
}
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"some-sort-of-identifier",
|
||||
@"identifier" : @"ABCDEFGHIJ",
|
||||
@"policy" : @"SILENT_BLOCKLIST",
|
||||
@"rule_type" : @"TEAMID",
|
||||
}];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
|
||||
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
|
||||
XCTAssertEqual(sut.type, SNTRuleTypeTeamID);
|
||||
XCTAssertEqual(sut.state, SNTRuleStateSilentBlock);
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"some-sort-of-identifier",
|
||||
@"identifier" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
@"policy" : @"ALLOWLIST_COMPILER",
|
||||
@"rule_type" : @"BINARY",
|
||||
}];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
|
||||
XCTAssertEqualObjects(sut.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(sut.type, SNTRuleTypeBinary);
|
||||
XCTAssertEqual(sut.state, SNTRuleStateAllowCompiler);
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"some-sort-of-identifier",
|
||||
@"identifier" : @"ABCDEFGHIJ",
|
||||
@"policy" : @"REMOVE",
|
||||
@"rule_type" : @"TEAMID",
|
||||
}];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
|
||||
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
|
||||
XCTAssertEqual(sut.type, SNTRuleTypeTeamID);
|
||||
XCTAssertEqual(sut.state, SNTRuleStateRemove);
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"some-sort-of-identifier",
|
||||
@"identifier" : @"ABCDEFGHIJ",
|
||||
@"policy" : @"ALLOWLIST",
|
||||
@"rule_type" : @"TEAMID",
|
||||
@"custom_msg" : @"A custom block message",
|
||||
@"custom_url" : @"https://example.com",
|
||||
}];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
|
||||
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
|
||||
XCTAssertEqual(sut.type, SNTRuleTypeTeamID);
|
||||
XCTAssertEqual(sut.state, SNTRuleStateAllow);
|
||||
XCTAssertEqualObjects(sut.customMsg, @"A custom block message");
|
||||
XCTAssertEqualObjects(sut.customURL, @"https://example.com");
|
||||
|
||||
// TeamIDs must be 10 chars in length
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"A",
|
||||
@"policy" : @"ALLOWLIST",
|
||||
@"rule_type" : @"TEAMID",
|
||||
}];
|
||||
XCTAssertNil(sut);
|
||||
|
||||
// TeamIDs must be only alphanumeric chars
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"ßßßßßßßßßß",
|
||||
@"policy" : @"ALLOWLIST",
|
||||
@"rule_type" : @"TEAMID",
|
||||
}];
|
||||
XCTAssertNil(sut);
|
||||
|
||||
// TeamIDs are converted to uppercase
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"abcdefghij",
|
||||
@"policy" : @"REMOVE",
|
||||
@"rule_type" : @"TEAMID",
|
||||
}];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
|
||||
|
||||
// SigningID tests
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"ABCDEFGHIJ:com.example",
|
||||
@"policy" : @"REMOVE",
|
||||
@"rule_type" : @"SIGNINGID",
|
||||
}];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ:com.example");
|
||||
XCTAssertEqual(sut.type, SNTRuleTypeSigningID);
|
||||
XCTAssertEqual(sut.state, SNTRuleStateRemove);
|
||||
|
||||
// Invalid SingingID tests:
|
||||
for (NSString *ident in @[
|
||||
@":com.example", // missing team ID
|
||||
@"ABCDEFGHIJ:", // missing signing ID
|
||||
@"ABC:com.example", // Invalid team id
|
||||
@":", // missing team and signing IDs
|
||||
@"", // empty string
|
||||
]) {
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : ident,
|
||||
@"policy" : @"REMOVE",
|
||||
@"rule_type" : @"SIGNINGID",
|
||||
}];
|
||||
XCTAssertNil(sut);
|
||||
}
|
||||
|
||||
// Signing ID with lower team ID has case fixed up
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"abcdefghij:com.example",
|
||||
@"policy" : @"REMOVE",
|
||||
@"rule_type" : @"SIGNINGID",
|
||||
}];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ:com.example");
|
||||
|
||||
// Signing ID with lower platform team ID is left alone
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"platform:com.example",
|
||||
@"policy" : @"REMOVE",
|
||||
@"rule_type" : @"SIGNINGID",
|
||||
}];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.identifier, @"platform:com.example");
|
||||
|
||||
// Signing ID can contain the TID:SID delimiter character (":")
|
||||
for (NSString *ident in @[
|
||||
@"ABCDEFGHIJ:com:",
|
||||
@"ABCDEFGHIJ:com:example",
|
||||
@"ABCDEFGHIJ::",
|
||||
@"ABCDEFGHIJ:com:example:with:more:components:",
|
||||
]) {
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : ident,
|
||||
@"policy" : @"ALLOWLIST",
|
||||
@"rule_type" : @"SIGNINGID",
|
||||
}];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.identifier, ident);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testInitWithDictionaryInvalid {
|
||||
@@ -94,12 +200,19 @@
|
||||
XCTAssertNil(sut);
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"an-identifier",
|
||||
@"identifier" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
}];
|
||||
XCTAssertNil(sut);
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"an-identifier",
|
||||
@"policy" : @"ALLOWLIST",
|
||||
@"rule_type" : @"BINARY",
|
||||
}];
|
||||
XCTAssertNil(sut);
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
@"policy" : @"OTHERPOLICY",
|
||||
@"rule_type" : @"BINARY",
|
||||
}];
|
||||
@@ -113,4 +226,63 @@
|
||||
XCTAssertNil(sut);
|
||||
}
|
||||
|
||||
- (void)testRuleDictionaryRepresentation {
|
||||
NSDictionary *expectedTeamID = @{
|
||||
@"identifier" : @"ABCDEFGHIJ",
|
||||
@"policy" : @"ALLOWLIST",
|
||||
@"rule_type" : @"TEAMID",
|
||||
@"custom_msg" : @"A custom block message",
|
||||
@"custom_url" : @"https://example.com",
|
||||
};
|
||||
|
||||
SNTRule *sut = [[SNTRule alloc] initWithDictionary:expectedTeamID];
|
||||
NSDictionary *dict = [sut dictionaryRepresentation];
|
||||
XCTAssertEqualObjects(expectedTeamID, dict);
|
||||
|
||||
NSDictionary *expectedBinary = @{
|
||||
@"identifier" : @"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6",
|
||||
@"policy" : @"BLOCKLIST",
|
||||
@"rule_type" : @"BINARY",
|
||||
@"custom_msg" : @"",
|
||||
@"custom_url" : @"",
|
||||
};
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:expectedBinary];
|
||||
dict = [sut dictionaryRepresentation];
|
||||
|
||||
XCTAssertEqualObjects(expectedBinary, dict);
|
||||
}
|
||||
|
||||
- (void)testRuleStateToPolicyString {
|
||||
NSDictionary *expected = @{
|
||||
@"identifier" : @"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6",
|
||||
@"policy" : @"ALLOWLIST",
|
||||
@"rule_type" : @"BINARY",
|
||||
@"custom_msg" : @"A custom block message",
|
||||
@"custom_url" : @"https://example.com",
|
||||
};
|
||||
|
||||
SNTRule *sut = [[SNTRule alloc] initWithDictionary:expected];
|
||||
sut.state = SNTRuleStateBlock;
|
||||
XCTAssertEqualObjects(kRulePolicyBlocklist, [sut dictionaryRepresentation][kRulePolicy]);
|
||||
sut.state = SNTRuleStateSilentBlock;
|
||||
XCTAssertEqualObjects(kRulePolicySilentBlocklist, [sut dictionaryRepresentation][kRulePolicy]);
|
||||
sut.state = SNTRuleStateAllow;
|
||||
XCTAssertEqualObjects(kRulePolicyAllowlist, [sut dictionaryRepresentation][kRulePolicy]);
|
||||
sut.state = SNTRuleStateAllowCompiler;
|
||||
XCTAssertEqualObjects(kRulePolicyAllowlistCompiler, [sut dictionaryRepresentation][kRulePolicy]);
|
||||
// Invalid states
|
||||
sut.state = SNTRuleStateRemove;
|
||||
XCTAssertEqualObjects(kRulePolicyRemove, [sut dictionaryRepresentation][kRulePolicy]);
|
||||
}
|
||||
|
||||
/*
|
||||
- (void)testRuleTypeToString {
|
||||
SNTRule *sut = [[SNTRule alloc] init];
|
||||
XCTAssertEqual(kRuleTypeBinary, [sut ruleTypeToString:@""]);//SNTRuleTypeBinary]);
|
||||
XCTAssertEqual(kRuleTypeCertificate,[sut ruleTypeToString:SNTRuleTypeCertificate]);
|
||||
XCTAssertEqual(kRuleTypeTeamID, [sut ruleTypeToString:SNTRuleTypeTeamID]);
|
||||
XCTAssertEqual(kRuleTypeSigningID,[sut ruleTypeToString:SNTRuleTypeSigningID]);
|
||||
}*/
|
||||
|
||||
@end
|
||||
|
||||
@@ -100,6 +100,11 @@
|
||||
///
|
||||
@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.
|
||||
///
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
|
||||
ENCODE(self.signingChain, @"signingChain");
|
||||
ENCODE(self.teamID, @"teamID");
|
||||
ENCODE(self.signingID, @"signingID");
|
||||
|
||||
ENCODE(self.executingUser, @"executingUser");
|
||||
ENCODE(self.occurrenceDate, @"occurrenceDate");
|
||||
@@ -95,10 +96,11 @@
|
||||
|
||||
_signingChain = DECODEARRAY(MOLCertificate, @"signingChain");
|
||||
_teamID = DECODE(NSString, @"teamID");
|
||||
_signingID = DECODE(NSString, @"signingID");
|
||||
|
||||
_executingUser = DECODE(NSString, @"executingUser");
|
||||
_occurrenceDate = DECODE(NSDate, @"occurrenceDate");
|
||||
_decision = (SNTEventState)[DECODE(NSNumber, @"decision") intValue];
|
||||
_decision = (SNTEventState)[DECODE(NSNumber, @"decision") unsignedLongLongValue];
|
||||
_pid = DECODE(NSNumber, @"pid");
|
||||
_ppid = DECODE(NSNumber, @"ppid");
|
||||
_parentName = DECODE(NSString, @"parentName");
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
extern NSString *const kXSRFToken;
|
||||
extern NSString *const kDefaultXSRFTokenHeader;
|
||||
extern NSString *const kXSRFTokenHeader;
|
||||
|
||||
extern NSString *const kSerialNumber;
|
||||
extern NSString *const kHostname;
|
||||
@@ -31,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;
|
||||
@@ -41,6 +43,7 @@ extern NSString *const kCertificateRuleCount;
|
||||
extern NSString *const kCompilerRuleCount;
|
||||
extern NSString *const kTransitiveRuleCount;
|
||||
extern NSString *const kTeamIDRuleCount;
|
||||
extern NSString *const kSigningIDRuleCount;
|
||||
extern NSString *const kFullSyncInterval;
|
||||
extern NSString *const kFCMToken;
|
||||
extern NSString *const kFCMFullSyncInterval;
|
||||
@@ -52,6 +55,7 @@ extern NSString *const kEnableTransitiveRulesDeprecated;
|
||||
extern NSString *const kEnableTransitiveRulesSuperDeprecated;
|
||||
extern NSString *const kEnableAllEventUpload;
|
||||
extern NSString *const kDisableUnknownEventUpload;
|
||||
extern NSString *const kOverrideFileAccessAction;
|
||||
|
||||
extern NSString *const kEvents;
|
||||
extern NSString *const kFileSHA256;
|
||||
@@ -65,11 +69,13 @@ extern NSString *const kDecisionAllowBinary;
|
||||
extern NSString *const kDecisionAllowCertificate;
|
||||
extern NSString *const kDecisionAllowScope;
|
||||
extern NSString *const kDecisionAllowTeamID;
|
||||
extern NSString *const kDecisionAllowSigningID;
|
||||
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 kDecisionUnknown;
|
||||
extern NSString *const kDecisionBundleBinary;
|
||||
extern NSString *const kLoggedInUsers;
|
||||
@@ -94,6 +100,7 @@ extern NSString *const kCertOU;
|
||||
extern NSString *const kCertValidFrom;
|
||||
extern NSString *const kCertValidUntil;
|
||||
extern NSString *const kTeamID;
|
||||
extern NSString *const kSigningID;
|
||||
extern NSString *const kQuarantineDataURL;
|
||||
extern NSString *const kQuarantineRefererURL;
|
||||
extern NSString *const kQuarantineTimestamp;
|
||||
@@ -117,7 +124,9 @@ extern NSString *const kRuleType;
|
||||
extern NSString *const kRuleTypeBinary;
|
||||
extern NSString *const kRuleTypeCertificate;
|
||||
extern NSString *const kRuleTypeTeamID;
|
||||
extern NSString *const kRuleTypeSigningID;
|
||||
extern NSString *const kRuleCustomMsg;
|
||||
extern NSString *const kRuleCustomURL;
|
||||
extern NSString *const kCursor;
|
||||
|
||||
extern NSString *const kBackoffInterval;
|
||||
@@ -129,6 +138,9 @@ extern NSString *const kLogSync;
|
||||
|
||||
extern const NSUInteger kDefaultEventBatchSize;
|
||||
|
||||
extern NSString *const kPostflightRulesReceived;
|
||||
extern NSString *const kPostflightRulesProcessed;
|
||||
|
||||
///
|
||||
/// kDefaultFullSyncInterval
|
||||
/// kDefaultFCMFullSyncInterval
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
|
||||
#import "Source/common/SNTSyncConstants.h"
|
||||
|
||||
NSString *const kXSRFToken = @"X-XSRF-TOKEN";
|
||||
NSString *const kDefaultXSRFTokenHeader = @"X-XSRF-TOKEN";
|
||||
NSString *const kXSRFTokenHeader = @"X-XSRF-TOKEN-HEADER";
|
||||
|
||||
NSString *const kSerialNumber = @"serial_num";
|
||||
NSString *const kHostname = @"hostname";
|
||||
@@ -31,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";
|
||||
@@ -41,10 +43,12 @@ NSString *const kCertificateRuleCount = @"certificate_rule_count";
|
||||
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 kFullSyncInterval = @"full_sync_interval";
|
||||
NSString *const kFCMToken = @"fcm_token";
|
||||
NSString *const kFCMFullSyncInterval = @"fcm_full_sync_interval";
|
||||
NSString *const kFCMGlobalRuleSyncDeadline = @"fcm_global_rule_sync_deadline";
|
||||
NSString *const kOverrideFileAccessAction = @"override_file_access_action";
|
||||
|
||||
NSString *const kEnableBundles = @"enable_bundles";
|
||||
NSString *const kEnableBundlesDeprecated = @"bundles_enabled";
|
||||
@@ -66,11 +70,13 @@ NSString *const kDecisionAllowBinary = @"ALLOW_BINARY";
|
||||
NSString *const kDecisionAllowCertificate = @"ALLOW_CERTIFICATE";
|
||||
NSString *const kDecisionAllowScope = @"ALLOW_SCOPE";
|
||||
NSString *const kDecisionAllowTeamID = @"ALLOW_TEAMID";
|
||||
NSString *const kDecisionAllowSigningID = @"ALLOW_SIGNINGID";
|
||||
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 kDecisionUnknown = @"UNKNOWN";
|
||||
NSString *const kDecisionBundleBinary = @"BUNDLE_BINARY";
|
||||
NSString *const kLoggedInUsers = @"logged_in_users";
|
||||
@@ -95,6 +101,7 @@ NSString *const kCertOU = @"ou";
|
||||
NSString *const kCertValidFrom = @"valid_from";
|
||||
NSString *const kCertValidUntil = @"valid_until";
|
||||
NSString *const kTeamID = @"team_id";
|
||||
NSString *const kSigningID = @"signing_id";
|
||||
NSString *const kQuarantineDataURL = @"quarantine_data_url";
|
||||
NSString *const kQuarantineRefererURL = @"quarantine_referer_url";
|
||||
NSString *const kQuarantineTimestamp = @"quarantine_timestamp";
|
||||
@@ -118,7 +125,9 @@ NSString *const kRuleType = @"rule_type";
|
||||
NSString *const kRuleTypeBinary = @"BINARY";
|
||||
NSString *const kRuleTypeCertificate = @"CERTIFICATE";
|
||||
NSString *const kRuleTypeTeamID = @"TEAMID";
|
||||
NSString *const kRuleTypeSigningID = @"SIGNINGID";
|
||||
NSString *const kRuleCustomMsg = @"custom_msg";
|
||||
NSString *const kRuleCustomURL = @"custom_url";
|
||||
NSString *const kCursor = @"cursor";
|
||||
|
||||
NSString *const kBackoffInterval = @"backoff";
|
||||
@@ -128,6 +137,9 @@ NSString *const kRuleSync = @"rule_sync";
|
||||
NSString *const kConfigSync = @"config_sync";
|
||||
NSString *const kLogSync = @"log_sync";
|
||||
|
||||
NSString *const kPostflightRulesReceived = @"rules_received";
|
||||
NSString *const kPostflightRulesProcessed = @"rules_processed";
|
||||
|
||||
const NSUInteger kDefaultEventBatchSize = 50;
|
||||
const NSUInteger kDefaultFullSyncInterval = 600;
|
||||
const NSUInteger kDefaultPushNotificationsFullSyncInterval = 14400;
|
||||
|
||||
@@ -28,14 +28,16 @@
|
||||
/// 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)retrieveAllRules:(void (^)(NSArray<SNTRule *> *rules, NSError *error))reply;
|
||||
|
||||
///
|
||||
/// Config ops
|
||||
@@ -43,7 +45,7 @@
|
||||
- (void)setClientMode:(SNTClientMode)mode reply:(void (^)(void))reply;
|
||||
- (void)setFullSyncLastSuccess:(NSDate *)date reply:(void (^)(void))reply;
|
||||
- (void)setRuleSyncLastSuccess:(NSDate *)date reply:(void (^)(void))reply;
|
||||
- (void)setSyncCleanRequired:(BOOL)cleanReqd reply:(void (^)(void))reply;
|
||||
- (void)setSyncTypeRequired:(SNTSyncType)syncType reply:(void (^)(void))reply;
|
||||
- (void)setAllowedPathRegex:(NSString *)pattern reply:(void (^)(void))reply;
|
||||
- (void)setBlockedPathRegex:(NSString *)pattern reply:(void (^)(void))reply;
|
||||
- (void)setBlockUSBMount:(BOOL)enabled reply:(void (^)(void))reply;
|
||||
@@ -52,6 +54,7 @@
|
||||
- (void)setEnableTransitiveRules:(BOOL)enabled reply:(void (^)(void))reply;
|
||||
- (void)setEnableAllEventUpload:(BOOL)enabled reply:(void (^)(void))reply;
|
||||
- (void)setDisableUnknownEventUpload:(BOOL)enabled reply:(void (^)(void))reply;
|
||||
- (void)setOverrideFileAccessAction:(NSString *)action reply:(void (^)(void))reply;
|
||||
|
||||
///
|
||||
/// Syncd Ops
|
||||
|
||||
@@ -50,9 +50,14 @@ 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];
|
||||
|
||||
[r setClasses:[NSSet setWithObjects:[NSArray class], [SNTRule class], nil]
|
||||
forSelector:@selector(retrieveAllRules:)
|
||||
argumentIndex:0
|
||||
ofReply:YES];
|
||||
}
|
||||
|
||||
+ (NSXPCInterface *)controlInterface {
|
||||
|
||||
@@ -17,13 +17,20 @@
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTXPCBundleServiceInterface.h"
|
||||
|
||||
@class SNTStoredEvent;
|
||||
@class SNTDeviceEvent;
|
||||
@class SNTFileAccessEvent;
|
||||
@class SNTStoredEvent;
|
||||
|
||||
/// Protocol implemented by SantaGUI and utilized by santad
|
||||
@protocol SNTNotifierXPC
|
||||
- (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message;
|
||||
- (void)postBlockNotification:(SNTStoredEvent *)event
|
||||
withCustomMessage:(NSString *)message
|
||||
andCustomURL:(NSString *)url;
|
||||
- (void)postUSBBlockNotification:(SNTDeviceEvent *)event withCustomMessage:(NSString *)message;
|
||||
- (void)postFileAccessBlockNotification:(SNTFileAccessEvent *)event
|
||||
customMessage:(NSString *)message
|
||||
customURL:(NSString *)url
|
||||
customText:(NSString *)text API_AVAILABLE(macos(13.0));
|
||||
- (void)postClientModeNotification:(SNTClientMode)clientmode;
|
||||
- (void)postRuleSyncNotificationWithCustomMessage:(NSString *)message;
|
||||
- (void)updateCountsForEvent:(SNTStoredEvent *)event
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
/// Database ops
|
||||
///
|
||||
- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive, int64_t teamID))reply;
|
||||
int64_t transitive, int64_t teamID, int64_t signingID))reply;
|
||||
- (void)databaseEventCount:(void (^)(int64_t count))reply;
|
||||
- (void)staticRuleCount:(void (^)(int64_t count))reply;
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
fileSHA256:(NSString *)fileSHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
reply:(void (^)(SNTEventState))reply;
|
||||
|
||||
///
|
||||
@@ -67,9 +68,11 @@
|
||||
- (void)clientMode:(void (^)(SNTClientMode))reply;
|
||||
- (void)fullSyncLastSuccess:(void (^)(NSDate *))reply;
|
||||
- (void)ruleSyncLastSuccess:(void (^)(NSDate *))reply;
|
||||
- (void)syncCleanRequired:(void (^)(BOOL))reply;
|
||||
- (void)syncTypeRequired:(void (^)(SNTSyncType))reply;
|
||||
- (void)enableBundles:(void (^)(BOOL))reply;
|
||||
- (void)enableTransitiveRules:(void (^)(BOOL))reply;
|
||||
- (void)blockUSBMount:(void (^)(BOOL))reply;
|
||||
- (void)remountUSBMode:(void (^)(NSArray<NSString *> *))reply;
|
||||
|
||||
///
|
||||
/// Metrics ops
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
29
Source/common/ScopedCFTypeRef.h
Normal file
29
Source/common/ScopedCFTypeRef.h
Normal 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
|
||||
141
Source/common/ScopedCFTypeRefTest.mm
Normal file
141
Source/common/ScopedCFTypeRefTest.mm
Normal 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
|
||||
30
Source/common/ScopedIOObjectRef.h
Normal file
30
Source/common/ScopedIOObjectRef.h
Normal 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
|
||||
104
Source/common/ScopedIOObjectRefTest.mm
Normal file
104
Source/common/ScopedIOObjectRefTest.mm
Normal 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
|
||||
80
Source/common/ScopedTypeRef.h
Normal file
80
Source/common/ScopedTypeRef.h
Normal 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
|
||||
53
Source/common/String.h
Normal file
53
Source/common/String.h
Normal 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.
|
||||
|
||||
#ifndef SANTA__COMMON__STRING_H
|
||||
#define SANTA__COMMON__STRING_H
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace santa::common {
|
||||
|
||||
static inline std::string_view NSStringToUTF8StringView(NSString *str) {
|
||||
return std::string_view(str.UTF8String, [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
|
||||
}
|
||||
|
||||
static inline std::string NSStringToUTF8String(NSString *str) {
|
||||
return std::string(str.UTF8String, [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
|
||||
}
|
||||
|
||||
static inline NSString *StringToNSString(const std::string &str) {
|
||||
return [NSString stringWithUTF8String:str.c_str()];
|
||||
}
|
||||
|
||||
static inline NSString *StringToNSString(const char *str) {
|
||||
return [NSString stringWithUTF8String:str];
|
||||
}
|
||||
|
||||
static inline NSString *OptionalStringToNSString(const std::optional<std::string> &optional_str) {
|
||||
std::string str = optional_str.value_or("");
|
||||
if (str.length() == 0) {
|
||||
return nil;
|
||||
} else {
|
||||
return StringToNSString(str);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace santa::common
|
||||
|
||||
#endif
|
||||
@@ -35,6 +35,9 @@ uint64_t MachTimeToNanos(uint64_t mach_time);
|
||||
// Convert nanoseconds to mach absolute time
|
||||
uint64_t NanosToMachTime(uint64_t nanos);
|
||||
|
||||
// Add some number of nanoseconds to a given mach time and return the new result
|
||||
uint64_t AddNanosecondsToMachTime(uint64_t ns, uint64_t machTime);
|
||||
|
||||
// Get the result of proc_pidinfo with the PROC_PIDTASKINFO flavor
|
||||
std::optional<SantaTaskInfo> GetTaskInfo();
|
||||
|
||||
|
||||
@@ -39,17 +39,28 @@ static mach_timebase_info_data_t GetTimebase() {
|
||||
}
|
||||
|
||||
uint64_t MachTimeToNanos(uint64_t mach_time) {
|
||||
mach_timebase_info_data_t timebase = GetTimebase();
|
||||
static mach_timebase_info_data_t timebase = GetTimebase();
|
||||
|
||||
return mach_time * timebase.numer / timebase.denom;
|
||||
}
|
||||
|
||||
uint64_t NanosToMachTime(uint64_t nanos) {
|
||||
mach_timebase_info_data_t timebase = GetTimebase();
|
||||
static mach_timebase_info_data_t timebase = GetTimebase();
|
||||
|
||||
return nanos * timebase.denom / timebase.numer;
|
||||
}
|
||||
|
||||
uint64_t AddNanosecondsToMachTime(uint64_t ns, uint64_t machTime) {
|
||||
// Convert machtime to nanoseconds
|
||||
uint64_t nanoTime = MachTimeToNanos(machTime);
|
||||
|
||||
// Add the nanosecond offset
|
||||
nanoTime += ns;
|
||||
|
||||
// Convert back to machTime
|
||||
return NanosToMachTime(nanoTime);
|
||||
}
|
||||
|
||||
std::optional<SantaTaskInfo> GetTaskInfo() {
|
||||
struct proc_taskinfo pti;
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#define NOBODY_UID ((unsigned int)-2)
|
||||
#define NOGROUP_GID ((unsigned int)-1)
|
||||
|
||||
@@ -38,23 +40,33 @@
|
||||
// Pretty print C++ string match errors
|
||||
#define XCTAssertCppStringEqual(got, want) XCTAssertCStringEqual((got).c_str(), (want).c_str())
|
||||
|
||||
#define XCTAssertCppStringBeginsWith(got, want) \
|
||||
XCTAssertTrue((got).rfind((want), 0) == 0, "\nPrefix not found.\n\t got: %s\n\twant: %s\n", \
|
||||
(got).c_str(), (want).c_str())
|
||||
|
||||
// Note: Delta between local formatter and the one run on Github. Disable for now.
|
||||
// clang-format off
|
||||
#define XCTAssertSemaTrue(s, sec, m) \
|
||||
XCTAssertEqual( \
|
||||
0, dispatch_semaphore_wait((s), dispatch_time(DISPATCH_TIME_NOW, (sec)*NSEC_PER_SEC)), m)
|
||||
0, dispatch_semaphore_wait((s), dispatch_time(DISPATCH_TIME_NOW, (sec) * NSEC_PER_SEC)), m)
|
||||
// clang-format on
|
||||
|
||||
// Helper to ensure at least `ms` milliseconds are slept, even if the sleep
|
||||
// function returns early due to interrupts.
|
||||
void SleepMS(long ms);
|
||||
|
||||
enum class ActionType {
|
||||
Auth,
|
||||
Notify,
|
||||
};
|
||||
// Helper to construct strings of a given length
|
||||
NSString *RepeatedString(NSString *str, NSUInteger len);
|
||||
|
||||
//
|
||||
// Helpers to construct various ES structs
|
||||
//
|
||||
|
||||
enum class ActionType {
|
||||
Auth,
|
||||
Notify,
|
||||
};
|
||||
|
||||
audit_token_t MakeAuditToken(pid_t pid, pid_t pidver);
|
||||
|
||||
/// Construct a `struct stat` buffer with each member having a unique value.
|
||||
@@ -64,7 +76,7 @@ audit_token_t MakeAuditToken(pid_t pid, pid_t pidver);
|
||||
struct stat MakeStat(int offset = 0);
|
||||
|
||||
es_string_token_t MakeESStringToken(const char *s);
|
||||
es_file_t MakeESFile(const char *path, struct stat sb = {});
|
||||
es_file_t MakeESFile(const char *path, struct stat sb = MakeStat());
|
||||
es_process_t MakeESProcess(es_file_t *file, audit_token_t tok = {}, audit_token_t parent_tok = {});
|
||||
es_message_t MakeESMessage(es_event_type_t et, es_process_t *proc,
|
||||
ActionType action_type = ActionType::Notify,
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
#include <uuid/uuid.h>
|
||||
#include "Source/common/SystemResources.h"
|
||||
|
||||
NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
return [@"" stringByPaddingToLength:len withString:str startingAtIndex:0];
|
||||
}
|
||||
|
||||
audit_token_t MakeAuditToken(pid_t pid, pid_t pidver) {
|
||||
return audit_token_t{
|
||||
.val =
|
||||
@@ -87,18 +91,12 @@ es_process_t MakeESProcess(es_file_t *file, audit_token_t tok, audit_token_t par
|
||||
};
|
||||
}
|
||||
|
||||
static uint64_t AddMillisToMachTime(uint64_t ms, uint64_t machTime) {
|
||||
uint64_t nanoTime = MachTimeToNanos(machTime);
|
||||
|
||||
// Add the ms offset
|
||||
nanoTime += (ms * NSEC_PER_MSEC);
|
||||
|
||||
// Convert back to machTime
|
||||
return NanosToMachTime(nanoTime);
|
||||
}
|
||||
|
||||
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, *)) {
|
||||
@@ -115,7 +113,7 @@ uint32_t MaxSupportedESMessageVersionForCurrentOS() {
|
||||
es_message_t MakeESMessage(es_event_type_t et, es_process_t *proc, ActionType action_type,
|
||||
uint64_t future_deadline_ms) {
|
||||
es_message_t es_msg = {
|
||||
.deadline = AddMillisToMachTime(future_deadline_ms, mach_absolute_time()),
|
||||
.deadline = AddNanosecondsToMachTime(future_deadline_ms * NSEC_PER_MSEC, mach_absolute_time()),
|
||||
.process = proc,
|
||||
.action_type =
|
||||
(action_type == ActionType::Notify) ? ES_ACTION_TYPE_NOTIFY : ES_ACTION_TYPE_AUTH,
|
||||
|
||||
@@ -213,6 +213,27 @@ message CertificateInfo {
|
||||
optional string common_name = 2;
|
||||
}
|
||||
|
||||
// Information about a single entitlement key/value pair
|
||||
message Entitlement {
|
||||
// The name of an entitlement
|
||||
optional string key = 1;
|
||||
|
||||
// The value of an entitlement
|
||||
optional string value = 2;
|
||||
}
|
||||
|
||||
// Information about entitlements
|
||||
message EntitlementInfo {
|
||||
// Whether or not the set of reported entilements is complete or has been
|
||||
// filtered (e.g. by configuration or clipped because too many to log).
|
||||
optional bool entitlements_filtered = 1;
|
||||
|
||||
// The set of entitlements associated with the target executable
|
||||
// Only top level keys are represented
|
||||
// Values (including nested keys) are JSON serialized
|
||||
repeated Entitlement entitlements = 2;
|
||||
}
|
||||
|
||||
// Information about a process execution event
|
||||
message Execution {
|
||||
// The process that executed the new image (e.g. the process that called
|
||||
@@ -231,10 +252,10 @@ message Execution {
|
||||
optional FileInfo working_directory = 4;
|
||||
|
||||
// List of process arguments
|
||||
repeated string args = 5;
|
||||
repeated bytes args = 5;
|
||||
|
||||
// List of environment variables
|
||||
repeated string envs = 6;
|
||||
repeated bytes envs = 6;
|
||||
|
||||
// List of file descriptors
|
||||
repeated FileDescriptor fds = 7;
|
||||
@@ -262,6 +283,7 @@ message Execution {
|
||||
REASON_TRANSITIVE = 8;
|
||||
REASON_LONG_PATH = 9;
|
||||
REASON_NOT_RUNNING = 10;
|
||||
REASON_SIGNING_ID = 11;
|
||||
}
|
||||
optional Reason reason = 10;
|
||||
|
||||
@@ -285,6 +307,9 @@ message Execution {
|
||||
// The original path on disk of the target executable
|
||||
// Applies when executables are translocated
|
||||
optional string original_path = 15;
|
||||
|
||||
// Entitlement information about the target executbale
|
||||
optional EntitlementInfo entitlement_info = 16;
|
||||
}
|
||||
|
||||
// Information about a fork event
|
||||
@@ -380,6 +405,11 @@ message Unlink {
|
||||
optional FileInfo target = 2;
|
||||
}
|
||||
|
||||
// Information about a processes codesigning invalidation event
|
||||
message CodesigningInvalidated {
|
||||
optional ProcessInfoLight instigator = 1;
|
||||
}
|
||||
|
||||
// Information about a link event
|
||||
message Link {
|
||||
// The process performing the link
|
||||
@@ -428,6 +458,9 @@ message Disk {
|
||||
|
||||
// Time device appeared/disappeared
|
||||
optional google.protobuf.Timestamp appearance = 10;
|
||||
|
||||
// Path mounted from
|
||||
optional string mount_from = 11;
|
||||
}
|
||||
|
||||
// Information emitted when Santa captures bundle information
|
||||
@@ -525,6 +558,7 @@ message SantaMessage {
|
||||
Bundle bundle = 19;
|
||||
Allowlist allowlist = 20;
|
||||
FileAccess file_access = 21;
|
||||
CodesigningInvalidated codesigning_invalidated = 22;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,17 @@ swift_library(
|
||||
],
|
||||
)
|
||||
|
||||
swift_library(
|
||||
name = "SNTFileAccessMessageWindowView",
|
||||
srcs = [
|
||||
"SNTFileAccessMessageWindowView.swift",
|
||||
],
|
||||
generates_header = 1,
|
||||
deps = [
|
||||
"//Source/common:SNTFileAccessEvent",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SantaGUI_lib",
|
||||
srcs = [
|
||||
@@ -44,6 +55,8 @@ objc_library(
|
||||
"SNTBinaryMessageWindowController.m",
|
||||
"SNTDeviceMessageWindowController.h",
|
||||
"SNTDeviceMessageWindowController.m",
|
||||
"SNTFileAccessMessageWindowController.h",
|
||||
"SNTFileAccessMessageWindowController.m",
|
||||
"SNTMessageWindowController.h",
|
||||
"SNTMessageWindowController.m",
|
||||
"SNTNotificationManager.h",
|
||||
@@ -65,9 +78,12 @@ objc_library(
|
||||
deps = [
|
||||
":SNTAboutWindowView",
|
||||
":SNTDeviceMessageWindowView",
|
||||
":SNTFileAccessMessageWindowView",
|
||||
"//Source/common:CertificateHelpers",
|
||||
"//Source/common:SNTBlockMessage_SantaGUI",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDeviceEvent",
|
||||
"//Source/common:SNTFileAccessEvent",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SNTStrengthify",
|
||||
|
||||
@@ -244,7 +244,7 @@
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" priority="900" constant="112" id="Pec-Pa-4aZ"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="push" title="Open Event..." bezelStyle="rounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="X1b-TF-1TL">
|
||||
<buttonCell key="cell" type="push" title="Open..." bezelStyle="rounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="X1b-TF-1TL">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
///
|
||||
@interface SNTBinaryMessageWindowController : SNTMessageWindowController
|
||||
|
||||
- (instancetype)initWithEvent:(SNTStoredEvent *)event andMessage:(NSString *)message;
|
||||
- (instancetype)initWithEvent:(SNTStoredEvent *)event
|
||||
customMsg:(NSString *)message
|
||||
customURL:(NSString *)url;
|
||||
|
||||
- (IBAction)showCertInfo:(id)sender;
|
||||
- (void)updateBlockNotification:(SNTStoredEvent *)event withBundleHash:(NSString *)bundleHash;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import <SecurityInterface/SFCertificatePanel.h>
|
||||
|
||||
#import "Source/common/CertificateHelpers.h"
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
@@ -25,6 +26,9 @@
|
||||
/// The custom message to display for this event
|
||||
@property(copy) NSString *customMessage;
|
||||
|
||||
/// The custom URL to use for this event
|
||||
@property(copy) NSString *customURL;
|
||||
|
||||
/// A 'friendly' string representing the certificate information
|
||||
@property(readonly, nonatomic) NSString *publisherInfo;
|
||||
|
||||
@@ -39,11 +43,14 @@
|
||||
|
||||
@implementation SNTBinaryMessageWindowController
|
||||
|
||||
- (instancetype)initWithEvent:(SNTStoredEvent *)event andMessage:(NSString *)message {
|
||||
- (instancetype)initWithEvent:(SNTStoredEvent *)event
|
||||
customMsg:(NSString *)message
|
||||
customURL:(NSString *)url {
|
||||
self = [super initWithWindowNibName:@"MessageWindow"];
|
||||
if (self) {
|
||||
_event = event;
|
||||
_customMessage = message;
|
||||
_customURL = url;
|
||||
_progress = [NSProgress discreteProgressWithTotalUnitCount:1];
|
||||
[_progress addObserver:self
|
||||
forKeyPath:@"fractionCompleted"
|
||||
@@ -74,9 +81,14 @@
|
||||
|
||||
- (void)loadWindow {
|
||||
[super loadWindow];
|
||||
if (![[SNTConfigurator configurator] eventDetailURL]) {
|
||||
NSURL *url = [SNTBlockMessage eventDetailURLForEvent:self.event customURL:self.customURL];
|
||||
|
||||
if (!url) {
|
||||
[self.openEventButton removeFromSuperview];
|
||||
} else {
|
||||
} else if (self.customURL.length == 0) {
|
||||
// Set the button text only if a per-rule custom URL is not used. If a
|
||||
// custom URL is used, it is assumed that the `EventDetailText` config value
|
||||
// does not apply and the default text will be used.
|
||||
NSString *eventDetailText = [[SNTConfigurator configurator] eventDetailText];
|
||||
if (eventDetailText) {
|
||||
[self.openEventButton setTitle:eventDetailText];
|
||||
@@ -106,21 +118,17 @@
|
||||
|
||||
- (IBAction)showCertInfo:(id)sender {
|
||||
// SFCertificatePanel expects an NSArray of SecCertificateRef's
|
||||
NSMutableArray *certArray = [NSMutableArray arrayWithCapacity:[self.event.signingChain count]];
|
||||
for (MOLCertificate *cert in self.event.signingChain) {
|
||||
[certArray addObject:(id)cert.certRef];
|
||||
}
|
||||
|
||||
[[[SFCertificatePanel alloc] init] beginSheetForWindow:self.window
|
||||
modalDelegate:nil
|
||||
didEndSelector:nil
|
||||
contextInfo:nil
|
||||
certificates:certArray
|
||||
certificates:CertificateChain(self.event.signingChain)
|
||||
showGroup:YES];
|
||||
}
|
||||
|
||||
- (IBAction)openEventDetails:(id)sender {
|
||||
NSURL *url = [SNTBlockMessage eventDetailURLForEvent:self.event];
|
||||
NSURL *url = [SNTBlockMessage eventDetailURLForEvent:self.event customURL:self.customURL];
|
||||
|
||||
[self closeWindow:sender];
|
||||
[[NSWorkspace sharedWorkspace] openURL:url];
|
||||
}
|
||||
@@ -136,19 +144,7 @@
|
||||
}
|
||||
|
||||
- (NSString *)publisherInfo {
|
||||
MOLCertificate *leafCert = [self.event.signingChain firstObject];
|
||||
|
||||
if ([leafCert.commonName isEqualToString:@"Apple Mac OS Application Signing"]) {
|
||||
return [NSString stringWithFormat:@"App Store (Team ID: %@)", self.event.teamID];
|
||||
} else if (leafCert.commonName && leafCert.orgName) {
|
||||
return [NSString stringWithFormat:@"%@ - %@", leafCert.orgName, leafCert.commonName];
|
||||
} else if (leafCert.commonName) {
|
||||
return leafCert.commonName;
|
||||
} else if (leafCert.orgName) {
|
||||
return leafCert.orgName;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
return Publisher(self.event.signingChain, self.event.teamID);
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedCustomMessage {
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
/// 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 SwiftUI
|
||||
|
||||
import santa_common_SNTConfigurator
|
||||
@@ -37,7 +51,7 @@ struct SNTDeviceMessageWindowView: View {
|
||||
Text("Device Name").bold()
|
||||
Text("Device BSD Path").bold()
|
||||
|
||||
if event!.remountArgs.count > 0 {
|
||||
if event!.remountArgs?.count ?? 0 > 0 {
|
||||
Text("Remount Mode").bold()
|
||||
}
|
||||
}
|
||||
@@ -46,7 +60,7 @@ struct SNTDeviceMessageWindowView: View {
|
||||
Text(event!.mntonname)
|
||||
Text(event!.mntfromname)
|
||||
|
||||
if event!.remountArgs.count > 0 {
|
||||
if event!.remountArgs?.count ?? 0 > 0 {
|
||||
Text(event!.readableRemountArgs())
|
||||
}
|
||||
}
|
||||
|
||||
38
Source/gui/SNTFileAccessMessageWindowController.h
Normal file
38
Source/gui/SNTFileAccessMessageWindowController.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/// 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 <Cocoa/Cocoa.h>
|
||||
|
||||
#import "Source/gui/SNTMessageWindowController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class SNTFileAccessEvent;
|
||||
|
||||
///
|
||||
/// Controller for a single message window.
|
||||
///
|
||||
API_AVAILABLE(macos(13.0))
|
||||
@interface SNTFileAccessMessageWindowController : SNTMessageWindowController <NSWindowDelegate>
|
||||
|
||||
- (instancetype)initWithEvent:(SNTFileAccessEvent *)event
|
||||
customMessage:(nullable NSString *)message
|
||||
customURL:(nullable NSString *)url
|
||||
customText:(nullable NSString *)text;
|
||||
|
||||
@property(readonly) SNTFileAccessEvent *event;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
95
Source/gui/SNTFileAccessMessageWindowController.m
Normal file
95
Source/gui/SNTFileAccessMessageWindowController.m
Normal file
@@ -0,0 +1,95 @@
|
||||
/// 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/gui/SNTFileAccessMessageWindowController.h"
|
||||
#import "Source/gui/SNTFileAccessMessageWindowView-Swift.h"
|
||||
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
#import "Source/common/SNTFileAccessEvent.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
|
||||
@interface SNTFileAccessMessageWindowController ()
|
||||
@property NSString *customMessage;
|
||||
@property NSString *customURL;
|
||||
@property NSString *customText;
|
||||
@property SNTFileAccessEvent *event;
|
||||
@end
|
||||
|
||||
@implementation SNTFileAccessMessageWindowController
|
||||
|
||||
- (instancetype)initWithEvent:(SNTFileAccessEvent *)event
|
||||
customMessage:(nullable NSString *)message
|
||||
customURL:(nullable NSString *)url
|
||||
customText:(nullable NSString *)text {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_event = event;
|
||||
_customMessage = message;
|
||||
_customURL = url;
|
||||
_customText = text;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)showWindow:(id)sender {
|
||||
if (self.window) {
|
||||
[self.window orderOut:sender];
|
||||
}
|
||||
|
||||
self.window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 0, 0)
|
||||
styleMask:NSWindowStyleMaskBorderless
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
|
||||
self.window.contentViewController = [SNTFileAccessMessageWindowViewFactory
|
||||
createWithWindow:self.window
|
||||
event:self.event
|
||||
customMessage:self.attributedCustomMessage
|
||||
customURL:[SNTBlockMessage eventDetailURLForFileAccessEvent:self.event
|
||||
customURL:self.customURL]
|
||||
.absoluteString
|
||||
customText:self.customText
|
||||
uiStateCallback:^(BOOL preventNotificationsForADay) {
|
||||
self.silenceFutureNotifications = preventNotificationsForADay;
|
||||
}];
|
||||
|
||||
self.window.delegate = self;
|
||||
|
||||
// Make sure app doesn't appear in Cmd+Tab or Dock.
|
||||
NSApp.activationPolicy = NSApplicationActivationPolicyAccessory;
|
||||
|
||||
[super showWindow:sender];
|
||||
}
|
||||
|
||||
- (void)windowWillClose:(NSNotification *)notification {
|
||||
// Remove app from Cmd+Tab and Dock.
|
||||
NSApp.activationPolicy = NSApplicationActivationPolicyAccessory;
|
||||
[super windowWillClose:notification];
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedCustomMessage {
|
||||
return [SNTBlockMessage attributedBlockMessageForFileAccessEvent:self.event
|
||||
customMessage:self.customMessage];
|
||||
}
|
||||
|
||||
- (NSString *)messageHash {
|
||||
// The hash for display de-duplication/silencing purposes is a combination of:
|
||||
// 1. The current file access rule version
|
||||
// 2. The name of the rule that was violated
|
||||
// 3. The path of the process
|
||||
return [NSString
|
||||
stringWithFormat:@"%@|%@|%@", self.event.ruleVersion, self.event.ruleName, self.event.filePath];
|
||||
}
|
||||
|
||||
@end
|
||||
210
Source/gui/SNTFileAccessMessageWindowView.swift
Normal file
210
Source/gui/SNTFileAccessMessageWindowView.swift
Normal file
@@ -0,0 +1,210 @@
|
||||
/// 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 SecurityInterface
|
||||
import SwiftUI
|
||||
|
||||
import santa_common_SNTFileAccessEvent
|
||||
|
||||
@available(macOS 13, *)
|
||||
@objc public class SNTFileAccessMessageWindowViewFactory : NSObject {
|
||||
@objc public static func createWith(window: NSWindow,
|
||||
event: SNTFileAccessEvent,
|
||||
customMessage: NSAttributedString?,
|
||||
customURL: NSString?,
|
||||
customText: NSString?,
|
||||
uiStateCallback: ((Bool) -> Void)?) -> NSViewController {
|
||||
return NSHostingController(rootView:SNTFileAccessMessageWindowView(window:window,
|
||||
event:event,
|
||||
customMessage:customMessage,
|
||||
customURL:customURL as String?,
|
||||
customText:customText as String?,
|
||||
uiStateCallback:uiStateCallback)
|
||||
.frame(width:800, height:600))
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13, *)
|
||||
struct Property : View {
|
||||
var lbl: String
|
||||
var val: String
|
||||
var propertyAction: (() -> Void)? = nil
|
||||
|
||||
var body: some View {
|
||||
let width: CGFloat? = 150
|
||||
|
||||
HStack(spacing: 5) {
|
||||
HStack {
|
||||
if let block = propertyAction {
|
||||
Button(action: {
|
||||
block()
|
||||
}) {
|
||||
Image(systemName: "info.circle.fill")
|
||||
}.buttonStyle(BorderlessButtonStyle())
|
||||
}
|
||||
Text(lbl + ":")
|
||||
.frame(alignment: .trailing)
|
||||
.lineLimit(1)
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.padding(Edge.Set.horizontal, 10)
|
||||
}.frame(width: width, alignment: .trailing)
|
||||
|
||||
Text(val)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13, *)
|
||||
struct Event: View {
|
||||
let e: SNTFileAccessEvent
|
||||
let window: NSWindow?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing:10) {
|
||||
Property(lbl: "Path Accessed", val: e.accessedPath)
|
||||
Property(lbl: "Rule Name", val: e.ruleName)
|
||||
Property(lbl: "Rule Version", val: e.ruleVersion)
|
||||
|
||||
Divider()
|
||||
.frame(width: 700)
|
||||
|
||||
if let app = e.application {
|
||||
Property(lbl: "Application", val: app)
|
||||
}
|
||||
|
||||
if let pub = e.publisherInfo {
|
||||
Property(lbl: "Publisher", val: pub) {
|
||||
SFCertificatePanel.shared()
|
||||
.beginSheet(for: window,
|
||||
modalDelegate: nil,
|
||||
didEnd: nil,
|
||||
contextInfo: nil,
|
||||
certificates: e.signingChainCertRefs,
|
||||
showGroup: true)
|
||||
}
|
||||
}
|
||||
|
||||
Property(lbl: "Name", val: (e.filePath as NSString).lastPathComponent)
|
||||
Property(lbl: "Path", val: e.filePath)
|
||||
Property(lbl: "Identifier", val: e.fileSHA256)
|
||||
Property(lbl: "Parent", val: e.parentName + " (" + e.ppid.stringValue + ")")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13, *)
|
||||
struct SNTFileAccessMessageWindowView: View {
|
||||
let window: NSWindow?
|
||||
let event: SNTFileAccessEvent?
|
||||
let customMessage: NSAttributedString?
|
||||
let customURL: String?
|
||||
let customText: String?
|
||||
let uiStateCallback: ((Bool) -> Void)?
|
||||
|
||||
@Environment(\.openURL) var openURL
|
||||
@State public var checked = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing:20.0) {
|
||||
Spacer()
|
||||
Text("Santa").font(Font.custom("HelveticaNeue-UltraLight", size: 34.0))
|
||||
|
||||
if let msg = customMessage {
|
||||
Text(AttributedString(msg)).multilineTextAlignment(.center).padding(15.0)
|
||||
} else {
|
||||
Text("Access to a protected resource was denied.").multilineTextAlignment(.center).padding(15.0)
|
||||
}
|
||||
|
||||
Event(e: event!, window: window)
|
||||
|
||||
Toggle(isOn: $checked) {
|
||||
Text("Prevent future notifications for this application for a day")
|
||||
.font(Font.system(size: 11.0));
|
||||
}
|
||||
|
||||
VStack(spacing:15) {
|
||||
if customURL != nil {
|
||||
Button(action: openButton, label: {
|
||||
|
||||
Text(customText ?? "Open Event...").frame(maxWidth:.infinity)
|
||||
})
|
||||
}
|
||||
Button(action: dismissButton, label: {
|
||||
Text("Dismiss").frame(maxWidth:.infinity)
|
||||
})
|
||||
.keyboardShortcut(.return)
|
||||
}.frame(width: 220)
|
||||
|
||||
Spacer()
|
||||
|
||||
}.frame(maxWidth:800.0).fixedSize()
|
||||
}
|
||||
|
||||
func openButton() {
|
||||
guard let urlString = customURL else {
|
||||
print("No URL available")
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = URL(string: urlString) else {
|
||||
print("Failed to create URL")
|
||||
return
|
||||
}
|
||||
|
||||
openURL(url)
|
||||
}
|
||||
|
||||
func dismissButton() {
|
||||
if let block = uiStateCallback {
|
||||
block(self.checked)
|
||||
}
|
||||
window?.close()
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13, *)
|
||||
func testFileAccessEvent() -> SNTFileAccessEvent {
|
||||
let faaEvent = SNTFileAccessEvent()
|
||||
|
||||
faaEvent.accessedPath = "/accessed/path"
|
||||
faaEvent.ruleVersion = "watched_path.v1"
|
||||
faaEvent.ruleName = "watched_path"
|
||||
faaEvent.fileSHA256 = "b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
faaEvent.filePath = "/Applications/gShoe.app/Contents/MacOS/gShoe"
|
||||
faaEvent.application = "gShoe"
|
||||
faaEvent.teamID = "EQHXZ8M8AV"
|
||||
faaEvent.signingID = "com.google.gShoe"
|
||||
faaEvent.executingUser = "nobody"
|
||||
faaEvent.pid = 456
|
||||
faaEvent.ppid = 123
|
||||
faaEvent.parentName = "gLauncher"
|
||||
|
||||
return faaEvent
|
||||
}
|
||||
|
||||
// Enable previews in Xcode.
|
||||
@available(macOS 13, *)
|
||||
struct SNTFileAccessMessageWindowView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SNTFileAccessMessageWindowView(window: nil,
|
||||
event: testFileAccessEvent(),
|
||||
customMessage: nil,
|
||||
customURL: nil,
|
||||
customText: nil,
|
||||
uiStateCallback: nil)
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,6 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#import "Source/common/SNTXPCNotifierInterface.h"
|
||||
#import "Source/gui/SNTBinaryMessageWindowController.h"
|
||||
#import "Source/gui/SNTDeviceMessageWindowController.h"
|
||||
#import "Source/gui/SNTMessageWindowController.h"
|
||||
|
||||
///
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
#import "Source/common/SNTStrengthify.h"
|
||||
#import "Source/common/SNTSyncConstants.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/gui/SNTBinaryMessageWindowController.h"
|
||||
#import "Source/gui/SNTDeviceMessageWindowController.h"
|
||||
#import "Source/gui/SNTFileAccessMessageWindowController.h"
|
||||
#import "Source/gui/SNTMessageWindowController.h"
|
||||
|
||||
@interface SNTNotificationManager ()
|
||||
@@ -169,7 +172,8 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
|
||||
|
||||
[dc postNotificationName:@"com.google.santa.notification.blockedeexecution"
|
||||
object:@"com.google.santa"
|
||||
userInfo:userInfo];
|
||||
userInfo:userInfo
|
||||
deliverImmediately:YES];
|
||||
}
|
||||
|
||||
- (void)showQueuedWindow {
|
||||
@@ -319,14 +323,16 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
|
||||
[un addNotificationRequest:req withCompletionHandler:nil];
|
||||
}
|
||||
|
||||
- (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message {
|
||||
- (void)postBlockNotification:(SNTStoredEvent *)event
|
||||
withCustomMessage:(NSString *)message
|
||||
andCustomURL:(NSString *)url {
|
||||
if (!event) {
|
||||
LOGI(@"Error: Missing event object in message received from daemon!");
|
||||
return;
|
||||
}
|
||||
|
||||
SNTBinaryMessageWindowController *pendingMsg =
|
||||
[[SNTBinaryMessageWindowController alloc] initWithEvent:event andMessage:message];
|
||||
[[SNTBinaryMessageWindowController alloc] initWithEvent:event customMsg:message customURL:url];
|
||||
|
||||
[self queueMessage:pendingMsg];
|
||||
}
|
||||
@@ -342,6 +348,24 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
|
||||
[self queueMessage:pendingMsg];
|
||||
}
|
||||
|
||||
- (void)postFileAccessBlockNotification:(SNTFileAccessEvent *)event
|
||||
customMessage:(NSString *)message
|
||||
customURL:(NSString *)url
|
||||
customText:(NSString *)text API_AVAILABLE(macos(13.0)) {
|
||||
if (!event) {
|
||||
LOGI(@"Error: Missing event object in message received from daemon!");
|
||||
return;
|
||||
}
|
||||
|
||||
SNTFileAccessMessageWindowController *pendingMsg =
|
||||
[[SNTFileAccessMessageWindowController alloc] initWithEvent:event
|
||||
customMessage:message
|
||||
customURL:url
|
||||
customText:text];
|
||||
|
||||
[self queueMessage:pendingMsg];
|
||||
}
|
||||
|
||||
#pragma mark SNTBundleNotifierXPC protocol methods
|
||||
|
||||
- (void)updateCountsForEvent:(SNTStoredEvent *)event
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
id dncMock = OCMClassMock([NSDistributedNotificationCenter class]);
|
||||
OCMStub([dncMock defaultCenter]).andReturn(dncMock);
|
||||
|
||||
[sut postBlockNotification:ev withCustomMessage:@""];
|
||||
[sut postBlockNotification:ev withCustomMessage:@"" andCustomURL:@""];
|
||||
|
||||
OCMVerify([dncMock postNotificationName:@"com.google.santa.notification.blockedeexecution"
|
||||
object:@"com.google.santa"
|
||||
@@ -68,7 +68,8 @@
|
||||
XCTAssertEqualObjects(userInfo[@"ppid"], @1);
|
||||
XCTAssertEqualObjects(userInfo[@"execution_time"], @1660221048);
|
||||
return YES;
|
||||
}]]);
|
||||
}]
|
||||
deliverImmediately:YES]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -31,20 +31,22 @@ objc_library(
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:santa_cc_proto_library_wrapper",
|
||||
"//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:binaryproto_cc_proto_library_wrapper",
|
||||
"@com_google_protobuf//src/google/protobuf/json",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "santactl_lib",
|
||||
srcs = [
|
||||
"main.m",
|
||||
"Commands/SNTCommandFileInfo.m",
|
||||
"Commands/SNTCommandRule.m",
|
||||
"Commands/SNTCommandStatus.m",
|
||||
"Commands/SNTCommandVersion.m",
|
||||
"Commands/SNTCommandMetrics.h",
|
||||
"Commands/SNTCommandMetrics.m",
|
||||
"Commands/SNTCommandRule.h",
|
||||
"Commands/SNTCommandRule.m",
|
||||
"Commands/SNTCommandStatus.m",
|
||||
"Commands/SNTCommandSync.m",
|
||||
"Commands/SNTCommandVersion.m",
|
||||
"main.m",
|
||||
] + select({
|
||||
"//:opt_build": [],
|
||||
"//conditions:default": [
|
||||
@@ -114,6 +116,8 @@ santa_unit_test(
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SNTXPCBundleServiceInterface",
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
"@MOLCertificate",
|
||||
"@MOLCodesignChecker",
|
||||
@@ -144,11 +148,27 @@ santa_unit_test(
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTCommandRuleTest",
|
||||
srcs = [
|
||||
"Commands/SNTCommandRule.h",
|
||||
"Commands/SNTCommandRuleTest.mm",
|
||||
"SNTCommand.h",
|
||||
"SNTCommandController.h",
|
||||
],
|
||||
deps = [
|
||||
":santactl_lib",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTRule",
|
||||
],
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "unit_tests",
|
||||
tests = [
|
||||
":SNTCommandFileInfoTest",
|
||||
":SNTCommandMetricsTest",
|
||||
":SNTCommandRuleTest",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/common/SNTXPCBundleServiceInterface.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/santactl/SNTCommand.h"
|
||||
#import "Source/santactl/SNTCommandController.h"
|
||||
@@ -42,6 +44,7 @@ static NSString *const kRule = @"Rule";
|
||||
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";
|
||||
|
||||
// signing chain keys
|
||||
static NSString *const kCommonName = @"Common Name";
|
||||
@@ -54,6 +57,13 @@ static NSString *const kValidUntil = @"Valid Until";
|
||||
static NSString *const kSHA256 = @"SHA-256";
|
||||
static NSString *const kSHA1 = @"SHA-1";
|
||||
|
||||
// bundle info keys
|
||||
static NSString *const kBundleInfo = @"Bundle Info";
|
||||
static NSString *const kBundlePath = @"Main Bundle Path";
|
||||
static NSString *const kBundleID = @"Main Bundle ID";
|
||||
static NSString *const kBundleHash = @"Bundle Hash";
|
||||
static NSString *const kBundleHashes = @"Bundle Hashes";
|
||||
|
||||
// Message displayed when daemon communication fails
|
||||
static NSString *const kCommunicationErrorMsg = @"Could not communicate with daemon";
|
||||
|
||||
@@ -71,6 +81,7 @@ NSString *formattedStringForKeyArray(NSArray<NSString *> *array) {
|
||||
// Properties set from commandline flags
|
||||
@property(nonatomic) BOOL recursive;
|
||||
@property(nonatomic) BOOL jsonOutput;
|
||||
@property(nonatomic) BOOL bundleInfo;
|
||||
@property(nonatomic) NSNumber *certIndex;
|
||||
@property(nonatomic, copy) NSArray<NSString *> *outputKeyList;
|
||||
@property(nonatomic, copy) NSDictionary<NSString *, NSRegularExpression *> *outputFilters;
|
||||
@@ -111,6 +122,7 @@ typedef id (^SNTAttributeBlock)(SNTCommandFileInfo *, SNTFileInfo *);
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock downloadTimestamp;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock downloadAgent;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock teamID;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock signingID;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock type;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock pageZero;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock codeSigned;
|
||||
@@ -154,6 +166,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
@"\n"
|
||||
@"Usage: santactl fileinfo [options] [file-paths]\n"
|
||||
@" --recursive (-r): Search directories recursively.\n"
|
||||
@" Incompatible with --bundleinfo.\n"
|
||||
@" --json: Output in JSON format.\n"
|
||||
@" --key: Search and return this one piece of information.\n"
|
||||
@" You may specify multiple keys by repeating this flag.\n"
|
||||
@@ -165,12 +178,16 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
@" signing chain to show info only for that certificate.\n"
|
||||
@" 0 up to n for the leaf certificate up to the root\n"
|
||||
@" -1 down to -n-1 for the root certificate down to the leaf\n"
|
||||
@" Incompatible with --bundleinfo."
|
||||
@"\n"
|
||||
@" --filter: Use predicates of the form 'key=regex' to filter out which files\n"
|
||||
@" are displayed. Valid keys are the same as for --key. Value is a\n"
|
||||
@" case-insensitive regular expression which must match anywhere in\n"
|
||||
@" the keyed property value for the file's info to be displayed.\n"
|
||||
@" You may specify multiple filters by repeating this flag.\n"
|
||||
@" --bundleinfo: If the file is part of a bundle, will also display bundle\n"
|
||||
@" hash information and hashes of all bundle executables.\n"
|
||||
@" Incompatible with --recursive and --cert-index.\n"
|
||||
@"\n"
|
||||
@"Examples: santactl fileinfo --cert-index 1 --key SHA-256 --json /usr/bin/yes\n"
|
||||
@" santactl fileinfo --key SHA-256 --json /usr/bin/yes\n"
|
||||
@@ -184,8 +201,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
+ (NSArray<NSString *> *)fileInfoKeys {
|
||||
return @[
|
||||
kPath, kSHA256, kSHA1, kBundleName, kBundleVersion, kBundleVersionStr, kDownloadReferrerURL,
|
||||
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kType, kPageZero, kCodeSigned, kRule,
|
||||
kSigningChain, kUniversalSigningChain
|
||||
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kSigningID, kType, kPageZero,
|
||||
kCodeSigned, kRule, kSigningChain, kUniversalSigningChain
|
||||
];
|
||||
}
|
||||
|
||||
@@ -218,6 +235,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
kSigningChain : self.signingChain,
|
||||
kUniversalSigningChain : self.universalSigningChain,
|
||||
kTeamID : self.teamID,
|
||||
kSigningID : self.signingID,
|
||||
};
|
||||
|
||||
_printQueue = dispatch_queue_create("com.google.santactl.print_queue", DISPATCH_QUEUE_SERIAL);
|
||||
@@ -357,15 +375,34 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
NSError *err;
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:&err];
|
||||
[[cmd.daemonConn remoteObjectProxy]
|
||||
decisionForFilePath:fileInfo.path
|
||||
fileSHA256:fileInfo.SHA256
|
||||
certificateSHA256:err ? nil : csc.leafCertificate.SHA256
|
||||
teamID:[csc.signingInformation valueForKey:@"teamid"]
|
||||
reply:^(SNTEventState s) {
|
||||
state = s;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
|
||||
NSString *teamID =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier];
|
||||
NSString *identifier =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[[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);
|
||||
}];
|
||||
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
|
||||
cmd.daemonUnavailable = YES;
|
||||
return kCommunicationErrorMsg;
|
||||
@@ -381,6 +418,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
case SNTEventStateBlockCertificate: [output appendString:@" (Certificate)"]; break;
|
||||
case SNTEventStateAllowTeamID:
|
||||
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
|
||||
case SNTEventStateAllowSigningID:
|
||||
case SNTEventStateBlockSigningID: [output appendString:@" (SigningID)"]; break;
|
||||
case SNTEventStateAllowScope:
|
||||
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
|
||||
case SNTEventStateAllowCompiler: [output appendString:@" (Compiler)"]; break;
|
||||
@@ -473,6 +512,13 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
};
|
||||
}
|
||||
|
||||
- (SNTAttributeBlock)signingID {
|
||||
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
|
||||
return [csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
// Entry point for the command.
|
||||
@@ -651,6 +697,48 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
if (outputDict[key]) continue; // ignore keys that we've already set due to a filter
|
||||
outputDict[key] = self.propertyMap[key](self, fileInfo);
|
||||
}
|
||||
|
||||
if (self.bundleInfo) {
|
||||
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
|
||||
se.fileBundlePath = fileInfo.bundlePath;
|
||||
|
||||
MOLXPCConnection *bc = [SNTXPCBundleServiceInterface configuredConnection];
|
||||
[bc resume];
|
||||
|
||||
__block NSMutableDictionary *bundleInfo = [[NSMutableDictionary alloc] init];
|
||||
|
||||
bundleInfo[kBundlePath] = fileInfo.bundle.bundlePath;
|
||||
bundleInfo[kBundleID] = fileInfo.bundle.bundleIdentifier;
|
||||
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
[[bc remoteObjectProxy]
|
||||
hashBundleBinariesForEvent:se
|
||||
reply:^(NSString *hash, NSArray<SNTStoredEvent *> *events,
|
||||
NSNumber *time) {
|
||||
bundleInfo[kBundleHash] = hash;
|
||||
|
||||
NSMutableArray *bundleHashes = [[NSMutableArray alloc] init];
|
||||
|
||||
for (SNTStoredEvent *event in events) {
|
||||
[bundleHashes
|
||||
addObject:@{kSHA256 : event.fileSHA256, kPath : event.filePath}];
|
||||
}
|
||||
|
||||
bundleInfo[kBundleHashes] = bundleHashes;
|
||||
[[bc remoteObjectProxy] spindown];
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
|
||||
int secondsToWait = 30;
|
||||
if (dispatch_semaphore_wait(sema,
|
||||
dispatch_time(DISPATCH_TIME_NOW, secondsToWait * NSEC_PER_SEC))) {
|
||||
fprintf(stderr, "The bundle service did not finish collecting hashes within %d seconds\n",
|
||||
secondsToWait);
|
||||
}
|
||||
|
||||
outputDict[kBundleInfo] = bundleInfo;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's nothing in the outputDict, then don't need to print anything.
|
||||
@@ -679,6 +767,11 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (self.bundleInfo) {
|
||||
[output appendString:[self stringForBundleInfo:outputDict[kBundleInfo] key:kBundleInfo]];
|
||||
}
|
||||
|
||||
if (!singleKey) [output appendString:@"\n"];
|
||||
}
|
||||
|
||||
@@ -708,6 +801,9 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
if ([arg caseInsensitiveCompare:@"--json"] == NSOrderedSame) {
|
||||
self.jsonOutput = YES;
|
||||
} else if ([arg caseInsensitiveCompare:@"--cert-index"] == NSOrderedSame) {
|
||||
if (self.bundleInfo) {
|
||||
[self printErrorUsageAndExit:@"\n--cert-index is incompatible with --bundleinfo"];
|
||||
}
|
||||
i += 1; // advance to next argument and grab index
|
||||
if (i >= nargs || [arguments[i] hasPrefix:@"--"]) {
|
||||
[self printErrorUsageAndExit:@"\n--cert-index requires an argument"];
|
||||
@@ -757,7 +853,17 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
filters[key] = regex;
|
||||
} else if ([arg caseInsensitiveCompare:@"--recursive"] == NSOrderedSame ||
|
||||
[arg caseInsensitiveCompare:@"-r"] == NSOrderedSame) {
|
||||
if (self.bundleInfo) {
|
||||
[self printErrorUsageAndExit:@"\n--recursive is incompatible with --bundleinfo"];
|
||||
}
|
||||
self.recursive = YES;
|
||||
} else if ([arg caseInsensitiveCompare:@"--bundleinfo"] == NSOrderedSame ||
|
||||
[arg caseInsensitiveCompare:@"-b"] == NSOrderedSame) {
|
||||
if (self.recursive || self.certIndex) {
|
||||
[self printErrorUsageAndExit:
|
||||
@"\n--bundleinfo is incompatible with --recursive and --cert-index"];
|
||||
}
|
||||
self.bundleInfo = YES;
|
||||
} else {
|
||||
[paths addObject:arg];
|
||||
}
|
||||
@@ -837,6 +943,22 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
- (NSString *)stringForBundleInfo:(NSDictionary *)bundleInfo key:(NSString *)key {
|
||||
NSMutableString *result = [NSMutableString string];
|
||||
|
||||
[result appendFormat:@"%@:\n", key];
|
||||
|
||||
[result appendFormat:@" %-20s: %@\n", kBundlePath.UTF8String, bundleInfo[kBundlePath]];
|
||||
[result appendFormat:@" %-20s: %@\n", kBundleID.UTF8String, bundleInfo[kBundleID]];
|
||||
[result appendFormat:@" %-20s: %@\n", kBundleHash.UTF8String, bundleInfo[kBundleHash]];
|
||||
|
||||
for (NSDictionary *hashPath in bundleInfo[kBundleHashes]) {
|
||||
[result appendFormat:@" %@ %@\n", hashPath[kSHA256], hashPath[kPath]];
|
||||
}
|
||||
|
||||
return [result copy];
|
||||
}
|
||||
|
||||
- (NSString *)stringForCertificate:(NSDictionary *)cert withKeys:(NSArray *)keys index:(int)index {
|
||||
if (!cert) return @"";
|
||||
NSMutableString *result = [NSMutableString string];
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <google/protobuf/util/json_util.h>
|
||||
#include <google/protobuf/json/json.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <iostream>
|
||||
@@ -26,8 +26,8 @@
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/binaryproto_proto_include_wrapper.h"
|
||||
#include "google/protobuf/any.pb.h"
|
||||
|
||||
using google::protobuf::util::JsonPrintOptions;
|
||||
using google::protobuf::util::MessageToJsonString;
|
||||
using JsonPrintOptions = google::protobuf::json::PrintOptions;
|
||||
using google::protobuf::json::MessageToJsonString;
|
||||
using santa::fsspool::binaryproto::LogBatch;
|
||||
namespace pbv1 = ::santa::pb::v1;
|
||||
|
||||
|
||||
20
Source/santactl/Commands/SNTCommandRule.h
Normal file
20
Source/santactl/Commands/SNTCommandRule.h
Normal 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
|
||||
@@ -23,12 +23,10 @@
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/santactl/Commands/SNTCommandRule.h"
|
||||
#import "Source/santactl/SNTCommand.h"
|
||||
#import "Source/santactl/SNTCommandController.h"
|
||||
|
||||
@interface SNTCommandRule : SNTCommand <SNTCommandProtocol>
|
||||
@end
|
||||
|
||||
@implementation SNTCommandRule
|
||||
|
||||
REGISTER_COMMAND_NAME(@"rule")
|
||||
@@ -54,22 +52,50 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
@" --compiler: allow and mark as a compiler\n"
|
||||
@" --remove: remove existing rule\n"
|
||||
@" --check: check for an existing rule\n"
|
||||
@" --import {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}: identifier to add/remove/check\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"
|
||||
#ifdef DEBUG
|
||||
@" --force: allow manual changes even when SyncBaseUrl is set\n"
|
||||
#endif
|
||||
@" --message {message}: custom message\n");
|
||||
@" --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");
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
@@ -91,7 +117,10 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
newRule.type = SNTRuleTypeBinary;
|
||||
|
||||
NSString *path;
|
||||
NSString *jsonFilePath;
|
||||
BOOL check = NO;
|
||||
BOOL importRules = NO;
|
||||
BOOL exportRules = NO;
|
||||
|
||||
// Parse arguments
|
||||
for (NSUInteger i = 0; i < arguments.count; ++i) {
|
||||
@@ -116,6 +145,8 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
newRule.type = SNTRuleTypeCertificate;
|
||||
} else if ([arg caseInsensitiveCompare:@"--teamid"] == NSOrderedSame) {
|
||||
newRule.type = SNTRuleTypeTeamID;
|
||||
} else if ([arg caseInsensitiveCompare:@"--signingid"] == NSOrderedSame) {
|
||||
newRule.type = SNTRuleTypeSigningID;
|
||||
} else if ([arg caseInsensitiveCompare:@"--path"] == NSOrderedSame) {
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"--path requires an argument"];
|
||||
@@ -131,9 +162,6 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
[self printErrorUsageAndExit:@"--sha256 requires an argument"];
|
||||
}
|
||||
newRule.identifier = arguments[i];
|
||||
if (newRule.identifier.length != 64) {
|
||||
[self printErrorUsageAndExit:@"--sha256 requires a valid SHA-256 as the argument"];
|
||||
}
|
||||
} else if ([arg caseInsensitiveCompare:@"--message"] == NSOrderedSame) {
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"--message requires an argument"];
|
||||
@@ -143,14 +171,46 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
} else if ([arg caseInsensitiveCompare:@"--force"] == NSOrderedSame) {
|
||||
// Don't do anything special.
|
||||
#endif
|
||||
} else if ([arg caseInsensitiveCompare:@"--import"] == NSOrderedSame) {
|
||||
if (exportRules) {
|
||||
[self printErrorUsageAndExit:@"--import and --export are mutually exclusive"];
|
||||
}
|
||||
importRules = YES;
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"--import requires an argument"];
|
||||
}
|
||||
jsonFilePath = arguments[i];
|
||||
} else if ([arg caseInsensitiveCompare:@"--export"] == NSOrderedSame) {
|
||||
if (importRules) {
|
||||
[self printErrorUsageAndExit:@"--import and --export are mutually exclusive"];
|
||||
}
|
||||
exportRules = YES;
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"--export requires an argument"];
|
||||
}
|
||||
jsonFilePath = arguments[i];
|
||||
} else if ([arg caseInsensitiveCompare:@"--help"] == NSOrderedSame ||
|
||||
[arg caseInsensitiveCompare:@"-h"] == NSOrderedSame) {
|
||||
printf("%s\n", self.class.longHelpText.UTF8String);
|
||||
exit(0);
|
||||
} else {
|
||||
[self printErrorUsageAndExit:[@"Unknown argument: " stringByAppendingString:arg]];
|
||||
}
|
||||
}
|
||||
|
||||
if (check) {
|
||||
if (!newRule.identifier) return [self printErrorUsageAndExit:@"--check requires --identifier"];
|
||||
return [self printStateOfRule:newRule daemonConnection:self.daemonConn];
|
||||
if (jsonFilePath.length > 0) {
|
||||
if (importRules) {
|
||||
if (newRule.identifier != nil || path != nil || check) {
|
||||
[self printErrorUsageAndExit:@"--import can only be used by itself"];
|
||||
}
|
||||
[self importJSONFile:jsonFilePath];
|
||||
} else if (exportRules) {
|
||||
if (newRule.identifier != nil || path != nil || check) {
|
||||
[self printErrorUsageAndExit:@"--export can only be used by itself"];
|
||||
}
|
||||
[self exportJSONFile:jsonFilePath];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (path) {
|
||||
@@ -164,10 +224,25 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
} else if (newRule.type == SNTRuleTypeCertificate) {
|
||||
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
|
||||
newRule.identifier = cs.leafCertificate.SHA256;
|
||||
} else if (newRule.type == SNTRuleTypeTeamID) {
|
||||
} else if (newRule.type == SNTRuleTypeTeamID || newRule.type == SNTRuleTypeSigningID) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
if (newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate) {
|
||||
NSCharacterSet *nonHex =
|
||||
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
|
||||
if ([[newRule.identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length !=
|
||||
64) {
|
||||
[self printErrorUsageAndExit:@"BINARY or CERTIFICATE rules require a valid SHA-256"];
|
||||
}
|
||||
}
|
||||
|
||||
if (check) {
|
||||
if (!newRule.identifier) return [self printErrorUsageAndExit:@"--check requires --identifier"];
|
||||
return [self printStateOfRule:newRule daemonConnection:self.daemonConn];
|
||||
}
|
||||
|
||||
if (newRule.state == SNTRuleStateUnknown) {
|
||||
[self printErrorUsageAndExit:@"No state specified"];
|
||||
} else if (!newRule.identifier) {
|
||||
@@ -176,7 +251,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
databaseRuleAddRules:@[ newRule ]
|
||||
cleanSlate:NO
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
reply:^(NSError *error) {
|
||||
if (error) {
|
||||
printf("Failed to modify rules: %s",
|
||||
@@ -209,69 +284,201 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
}];
|
||||
}
|
||||
|
||||
// IMPORTANT: This method makes no attempt to validate whether or not the data
|
||||
// in a rule is valid. It merely constructs a string with the given data.
|
||||
// E.g., TeamID compiler rules are not currently supproted, but if a test rule
|
||||
// is provided with that state, an appropriate string will be returned.
|
||||
+ (NSString *)stringifyRule:(SNTRule *)rule withColor:(BOOL)colorize {
|
||||
NSMutableString *output;
|
||||
// Rule state is saved as eventState for output colorization down below
|
||||
SNTEventState eventState = SNTEventStateUnknown;
|
||||
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateUnknown:
|
||||
output = [@"No rule exists with the given parameters" mutableCopy];
|
||||
break;
|
||||
case SNTRuleStateAllow: OS_FALLTHROUGH;
|
||||
case SNTRuleStateAllowCompiler: OS_FALLTHROUGH;
|
||||
case SNTRuleStateAllowTransitive:
|
||||
output = [@"Allowed" mutableCopy];
|
||||
eventState = SNTEventStateAllow;
|
||||
break;
|
||||
case SNTRuleStateBlock: OS_FALLTHROUGH;
|
||||
case SNTRuleStateSilentBlock:
|
||||
output = [@"Blocked" mutableCopy];
|
||||
eventState = SNTEventStateBlock;
|
||||
break;
|
||||
case SNTRuleStateRemove: OS_FALLTHROUGH;
|
||||
default:
|
||||
output = [NSMutableString stringWithFormat:@"Unexpected rule state: %ld", rule.state];
|
||||
break;
|
||||
}
|
||||
|
||||
if (rule.state == SNTRuleStateUnknown) {
|
||||
// No more output to append
|
||||
return output;
|
||||
}
|
||||
|
||||
[output appendString:@" ("];
|
||||
|
||||
switch (rule.type) {
|
||||
case SNTRuleTypeUnknown: [output appendString:@"Unknown"]; break;
|
||||
case SNTRuleTypeBinary: [output appendString:@"Binary"]; break;
|
||||
case SNTRuleTypeSigningID: [output appendString:@"SigningID"]; break;
|
||||
case SNTRuleTypeCertificate: [output appendString:@"Certificate"]; break;
|
||||
case SNTRuleTypeTeamID: [output appendString:@"TeamID"]; break;
|
||||
default:
|
||||
output = [NSMutableString stringWithFormat:@"Unexpected rule type: %ld", rule.type];
|
||||
break;
|
||||
}
|
||||
|
||||
// Add additional attributes
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllowCompiler: [output appendString:@", Compiler"]; break;
|
||||
case SNTRuleStateAllowTransitive: [output appendString:@", Transitive"]; break;
|
||||
case SNTRuleStateSilentBlock: [output appendString:@", Silent"]; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
[output appendString:@")"];
|
||||
|
||||
// Colorize
|
||||
if (colorize) {
|
||||
if ((SNTEventStateAllow & eventState)) {
|
||||
[output insertString:@"\033[32m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
} else if ((SNTEventStateBlock & eventState)) {
|
||||
[output insertString:@"\033[31m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
} else {
|
||||
[output insertString:@"\033[33m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
}
|
||||
}
|
||||
|
||||
if (rule.state == SNTRuleStateAllowTransitive) {
|
||||
NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:rule.timestamp];
|
||||
[output appendString:[NSString stringWithFormat:@"\nlast access date: %@", [date description]]];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
- (void)printStateOfRule:(SNTRule *)rule daemonConnection:(MOLXPCConnection *)daemonConn {
|
||||
id<SNTDaemonControlXPC> rop = [daemonConn synchronousRemoteObjectProxy];
|
||||
NSString *fileSHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil;
|
||||
NSString *certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil;
|
||||
NSString *teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil;
|
||||
__block NSMutableString *output;
|
||||
[rop decisionForFilePath:nil
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
reply:^(SNTEventState s) {
|
||||
output =
|
||||
(SNTEventStateAllow & s) ? @"Allowed".mutableCopy : @"Blocked".mutableCopy;
|
||||
switch (s) {
|
||||
case SNTEventStateAllowUnknown:
|
||||
case SNTEventStateBlockUnknown: [output appendString:@" (Unknown)"]; break;
|
||||
case SNTEventStateAllowBinary:
|
||||
case SNTEventStateBlockBinary: [output appendString:@" (Binary)"]; break;
|
||||
case SNTEventStateAllowCertificate:
|
||||
case SNTEventStateBlockCertificate:
|
||||
[output appendString:@" (Certificate)"];
|
||||
break;
|
||||
case SNTEventStateAllowScope:
|
||||
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
|
||||
case SNTEventStateAllowCompiler:
|
||||
[output appendString:@" (Compiler)"];
|
||||
break;
|
||||
case SNTEventStateAllowTransitive:
|
||||
[output appendString:@" (Transitive)"];
|
||||
break;
|
||||
case SNTEventStateAllowTeamID:
|
||||
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
|
||||
default: output = @"None".mutableCopy; break;
|
||||
}
|
||||
if (isatty(STDOUT_FILENO)) {
|
||||
if ((SNTEventStateAllow & s)) {
|
||||
[output insertString:@"\033[32m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
} else if ((SNTEventStateBlock & s)) {
|
||||
[output insertString:@"\033[31m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
} else {
|
||||
[output insertString:@"\033[33m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
}
|
||||
}
|
||||
}];
|
||||
NSString *signingID = (rule.type == SNTRuleTypeSigningID) ? rule.identifier : nil;
|
||||
__block NSString *output;
|
||||
|
||||
[rop databaseRuleForBinarySHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
reply:^(SNTRule *r) {
|
||||
if (r.state == SNTRuleStateAllowTransitive) {
|
||||
NSDate *date =
|
||||
[NSDate dateWithTimeIntervalSinceReferenceDate:r.timestamp];
|
||||
[output appendString:[NSString
|
||||
stringWithFormat:@"\nlast access date: %@",
|
||||
[date description]]];
|
||||
}
|
||||
output = [SNTCommandRule stringifyRule:r
|
||||
withColor:(isatty(STDOUT_FILENO) == 1)];
|
||||
}];
|
||||
|
||||
printf("%s\n", output.UTF8String);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
- (void)importJSONFile:(NSString *)jsonFilePath {
|
||||
// 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];
|
||||
if (error) {
|
||||
[self printErrorUsageAndExit:[NSString stringWithFormat:@"Failed to read %@: %@", jsonFilePath,
|
||||
error.localizedDescription]];
|
||||
}
|
||||
|
||||
// We expect a JSON object with one key "rules". This is an array of rule
|
||||
// objects.
|
||||
// e.g.
|
||||
// {"rules": [{
|
||||
// "policy" : "BLOCKLIST",
|
||||
// "rule_type" : "BINARY",
|
||||
// "identifier" : "84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6"
|
||||
// "custom_url" : "",
|
||||
// "custom_msg" : "/bin/ls block for demo"
|
||||
// }]}
|
||||
NSDictionary *rules = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
|
||||
if (error) {
|
||||
[self printErrorUsageAndExit:[NSString stringWithFormat:@"Failed to parse %@: %@", jsonFilePath,
|
||||
error.localizedDescription]];
|
||||
}
|
||||
|
||||
NSMutableArray<SNTRule *> *parsedRules = [[NSMutableArray alloc] init];
|
||||
|
||||
for (NSDictionary *jsonRule in rules[@"rules"]) {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:jsonRule];
|
||||
if (!rule) {
|
||||
[self printErrorUsageAndExit:[NSString stringWithFormat:@"Invalid rule: %@", jsonRule]];
|
||||
}
|
||||
[parsedRules addObject:rule];
|
||||
}
|
||||
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
databaseRuleAddRules:parsedRules
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
reply:^(NSError *error) {
|
||||
if (error) {
|
||||
printf("Failed to modify rules: %s",
|
||||
[error.localizedDescription UTF8String]);
|
||||
LOGD(@"Failure reason: %@", error.localizedFailureReason);
|
||||
exit(1);
|
||||
}
|
||||
exit(0);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)exportJSONFile:(NSString *)jsonFilePath {
|
||||
// Get the rules from the daemon and then write them to the file.
|
||||
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
|
||||
[rop retrieveAllRules:^(NSArray<SNTRule *> *rules, NSError *error) {
|
||||
if (error) {
|
||||
printf("Failed to get rules: %s", [error.localizedDescription UTF8String]);
|
||||
LOGD(@"Failure reason: %@", error.localizedFailureReason);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (rules.count == 0) {
|
||||
printf("No rules to export.\n");
|
||||
exit(1);
|
||||
}
|
||||
// Convert Rules to an NSDictionary.
|
||||
NSMutableArray *rulesAsDicts = [[NSMutableArray alloc] init];
|
||||
|
||||
for (SNTRule *rule in rules) {
|
||||
// Omit transitive and remove rules as they're not relevant.
|
||||
if (rule.state == SNTRuleStateAllowTransitive || rule.state == SNTRuleStateRemove) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[rulesAsDicts addObject:[rule dictionaryRepresentation]];
|
||||
}
|
||||
|
||||
NSOutputStream *outputStream = [[NSOutputStream alloc] initToFileAtPath:jsonFilePath append:NO];
|
||||
[outputStream open];
|
||||
|
||||
// Write the rules to the file.
|
||||
// File should look like the following JSON:
|
||||
// {"rules": [{"policy": "ALLOWLIST", "identifier": hash, "rule_type: "BINARY"},}]}
|
||||
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:@{@"rules" : rulesAsDicts}
|
||||
options:NSJSONWritingPrettyPrinted
|
||||
error:&error];
|
||||
// Print error
|
||||
if (error) {
|
||||
printf("Failed to jsonify rules: %s", [error.localizedDescription UTF8String]);
|
||||
LOGD(@"Failure reason: %@", error.localizedFailureReason);
|
||||
exit(1);
|
||||
}
|
||||
// Write jsonData to the file
|
||||
[outputStream write:jsonData.bytes maxLength:jsonData.length];
|
||||
[outputStream close];
|
||||
exit(0);
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
96
Source/santactl/Commands/SNTCommandRuleTest.mm
Normal file
96
Source/santactl/Commands/SNTCommandRuleTest.mm
Normal 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
|
||||
@@ -15,11 +15,22 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/santactl/SNTCommand.h"
|
||||
#import "Source/santactl/SNTCommandController.h"
|
||||
|
||||
NSString *StartupOptionToString(SNTDeviceManagerStartupPreferences pref) {
|
||||
switch (pref) {
|
||||
case SNTDeviceManagerStartupPreferencesUnmount: return @"Unmount";
|
||||
case SNTDeviceManagerStartupPreferencesForceUnmount: return @"ForceUnmount";
|
||||
case SNTDeviceManagerStartupPreferencesRemount: return @"Remount";
|
||||
case SNTDeviceManagerStartupPreferencesForceRemount: return @"ForceRemount";
|
||||
default: return @"None";
|
||||
}
|
||||
}
|
||||
|
||||
@interface SNTCommandStatus : SNTCommand <SNTCommandProtocol>
|
||||
@end
|
||||
|
||||
@@ -45,7 +56,6 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
|
||||
|
||||
// Daemon status
|
||||
@@ -81,13 +91,19 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
}];
|
||||
|
||||
// Database counts
|
||||
__block int64_t eventCount = -1, binaryRuleCount = -1, certRuleCount = -1, teamIDRuleCount = -1;
|
||||
__block int64_t compilerRuleCount = -1, transitiveRuleCount = -1;
|
||||
__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 transitive, int64_t teamID, int64_t signingID) {
|
||||
binaryRuleCount = binary;
|
||||
certRuleCount = certificate;
|
||||
teamIDRuleCount = teamID;
|
||||
signingIDRuleCount = signingID;
|
||||
compilerRuleCount = compiler;
|
||||
transitiveRuleCount = transitive;
|
||||
}];
|
||||
@@ -113,8 +129,8 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
}];
|
||||
|
||||
__block BOOL syncCleanReqd = NO;
|
||||
[rop syncCleanRequired:^(BOOL clean) {
|
||||
syncCleanReqd = clean;
|
||||
[rop syncTypeRequired:^(SNTSyncType syncType) {
|
||||
syncCleanReqd = (syncType == SNTSyncTypeClean || syncType == SNTSyncTypeCleanAll);
|
||||
}];
|
||||
|
||||
__block BOOL pushNotifications = NO;
|
||||
@@ -152,10 +168,15 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
}
|
||||
}];
|
||||
|
||||
// Wait a maximum of 5s for stats collected from daemon to arrive.
|
||||
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5))) {
|
||||
fprintf(stderr, "Failed to retrieve some stats from daemon\n\n");
|
||||
}
|
||||
__block BOOL blockUSBMount = NO;
|
||||
[rop blockUSBMount:^(BOOL response) {
|
||||
blockUSBMount = response;
|
||||
}];
|
||||
|
||||
__block NSArray<NSString *> *remountUSBMode;
|
||||
[rop remountUSBMode:^(NSArray<NSString *> *response) {
|
||||
remountUSBMode = response;
|
||||
}];
|
||||
|
||||
// Format dates
|
||||
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
||||
@@ -179,20 +200,22 @@ 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),
|
||||
@"events_pending_upload" : @(eventCount),
|
||||
@@ -207,7 +230,6 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"last_successful_rule" : ruleSyncLastSuccessStr ?: @"null",
|
||||
@"push_notifications" : pushNotifications ? @"Connected" : @"Disconnected",
|
||||
@"bundle_scanning" : @(enableBundles),
|
||||
@"transitive_rules" : @(enableTransitiveRules),
|
||||
},
|
||||
} mutableCopy];
|
||||
|
||||
@@ -217,7 +239,7 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"enabled" : @(watchItemsEnabled),
|
||||
@"rule_count" : @(watchItemsRuleCount),
|
||||
@"policy_version" : watchItemsPolicyVersion,
|
||||
@"config_path" : watchItemsConfigPath,
|
||||
@"config_path" : watchItemsConfigPath ?: @"null",
|
||||
@"last_policy_update" : watchItemsLastUpdateStr ?: @"null",
|
||||
};
|
||||
} else {
|
||||
@@ -240,13 +262,20 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
} else {
|
||||
printf(">>> Daemon Info\n");
|
||||
printf(" %-25s | %s\n", "Mode", [clientMode UTF8String]);
|
||||
|
||||
if (enableTransitiveRules) {
|
||||
printf(" %-25s | %s\n", "Transitive Rules", (enableTransitiveRules ? "Yes" : "No"));
|
||||
}
|
||||
|
||||
printf(" %-25s | %s\n", "Log Type", [eventLogType UTF8String]);
|
||||
printf(" %-25s | %s\n", "File Logging", (fileLogging ? "Yes" : "No"));
|
||||
printf(" %-25s | %s\n", "USB Blocking", (configurator.blockUSBMount ? "Yes" : "No"));
|
||||
if (configurator.blockUSBMount && configurator.remountUSBMode.count > 0) {
|
||||
printf(" %-25s | %s\n", "USB Remounting Mode:",
|
||||
[[configurator.remountUSBMode componentsJoinedByString:@", "] UTF8String]);
|
||||
printf(" %-25s | %s\n", "USB Blocking", (blockUSBMount ? "Yes" : "No"));
|
||||
if (blockUSBMount && remountUSBMode.count > 0) {
|
||||
printf(" %-25s | %s\n", "USB Remounting Mode",
|
||||
[[remountUSBMode componentsJoinedByString:@", "] UTF8String]);
|
||||
}
|
||||
printf(" %-25s | %s\n", "On Start USB Options",
|
||||
StartupOptionToString(configurator.onStartUSBOptions).UTF8String);
|
||||
printf(" %-25s | %lld (Peak: %.2f%%)\n", "Watchdog CPU Events", cpuEvents, cpuPeak);
|
||||
printf(" %-25s | %lld (Peak: %.2fMB)\n", "Watchdog RAM Events", ramEvents, ramPeak);
|
||||
|
||||
@@ -258,6 +287,7 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
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", "Events Pending Upload", eventCount);
|
||||
@@ -272,7 +302,7 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
if (watchItemsEnabled) {
|
||||
printf(" %-25s | %s\n", "Policy Version", watchItemsPolicyVersion.UTF8String);
|
||||
printf(" %-25s | %llu\n", "Rule Count", watchItemsRuleCount);
|
||||
printf(" %-25s | %s\n", "Config Path", watchItemsConfigPath.UTF8String);
|
||||
printf(" %-25s | %s\n", "Config Path", (watchItemsConfigPath ?: @"(embedded)").UTF8String);
|
||||
printf(" %-25s | %s\n", "Last Policy Update", watchItemsLastUpdateStr.UTF8String);
|
||||
}
|
||||
|
||||
@@ -285,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) {
|
||||
|
||||
@@ -32,7 +32,7 @@ REGISTER_COMMAND_NAME(@"sync")
|
||||
#pragma mark SNTCommand protocol methods
|
||||
|
||||
+ (BOOL)requiresRoot {
|
||||
return YES;
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (BOOL)requiresDaemonConn {
|
||||
@@ -47,8 +47,10 @@ REGISTER_COMMAND_NAME(@"sync")
|
||||
return (@"If Santa is configured to synchronize with a server, "
|
||||
@"this is the command used for syncing.\n\n"
|
||||
@"Options:\n"
|
||||
@" --clean: Perform a clean sync, erasing all existing rules and requesting a\n"
|
||||
@" clean sync from the server.");
|
||||
@" --clean: Perform a clean sync, erasing all existing non-transitive rules and\n"
|
||||
@" requesting a clean sync from the server.\n"
|
||||
@" --clean-all: Perform a clean sync, erasing all existing rules and requesting a\n"
|
||||
@" clean sync from the server.");
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
@@ -75,10 +77,17 @@ REGISTER_COMMAND_NAME(@"sync")
|
||||
lr.unprivilegedInterface =
|
||||
[NSXPCInterface interfaceWithProtocol:@protocol(SNTSyncServiceLogReceiverXPC)];
|
||||
[lr resume];
|
||||
BOOL isClean = [NSProcessInfo.processInfo.arguments containsObject:@"--clean"];
|
||||
|
||||
SNTSyncType syncType = SNTSyncTypeNormal;
|
||||
if ([NSProcessInfo.processInfo.arguments containsObject:@"--clean-all"]) {
|
||||
syncType = SNTSyncTypeCleanAll;
|
||||
} else if ([NSProcessInfo.processInfo.arguments containsObject:@"--clean"]) {
|
||||
syncType = SNTSyncTypeClean;
|
||||
}
|
||||
|
||||
[[ss remoteObjectProxy]
|
||||
syncWithLogListener:logListener.endpoint
|
||||
isClean:isClean
|
||||
syncType:syncType
|
||||
reply:^(SNTSyncStatusType status) {
|
||||
if (status == SNTSyncStatusTypeTooManySyncsInProgress) {
|
||||
[self didReceiveLog:@"Too many syncs in progress, try again later."];
|
||||
|
||||
@@ -21,6 +21,9 @@ objc_library(
|
||||
name = "SNTRuleTable",
|
||||
srcs = ["DataLayer/SNTRuleTable.m"],
|
||||
hdrs = ["DataLayer/SNTRuleTable.h"],
|
||||
sdk_dylibs = [
|
||||
"EndpointSecurity",
|
||||
],
|
||||
deps = [
|
||||
":SNTDatabaseTable",
|
||||
"//Source/common:Platform",
|
||||
@@ -49,6 +52,7 @@ objc_library(
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:String",
|
||||
"//Source/common:Unit",
|
||||
],
|
||||
)
|
||||
@@ -198,6 +202,7 @@ objc_library(
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDeepCopy",
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTRule",
|
||||
@@ -208,6 +213,19 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "TTYWriter",
|
||||
srcs = ["TTYWriter.mm"],
|
||||
hdrs = ["TTYWriter.h"],
|
||||
sdk_dylibs = [
|
||||
"EndpointSecurity",
|
||||
],
|
||||
deps = [
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:String",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTExecutionController",
|
||||
srcs = ["SNTExecutionController.mm"],
|
||||
@@ -220,11 +238,14 @@ objc_library(
|
||||
":SNTPolicyProcessor",
|
||||
":SNTRuleTable",
|
||||
":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",
|
||||
@@ -232,7 +253,10 @@ objc_library(
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:String",
|
||||
"//Source/common:Unit",
|
||||
"@MOLCodesignChecker",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -284,7 +308,9 @@ objc_library(
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:String",
|
||||
"//Source/common:Unit",
|
||||
],
|
||||
)
|
||||
@@ -330,6 +356,9 @@ objc_library(
|
||||
name = "SNTEndpointSecurityFileAccessAuthorizer",
|
||||
srcs = ["EventProviders/SNTEndpointSecurityFileAccessAuthorizer.mm"],
|
||||
hdrs = ["EventProviders/SNTEndpointSecurityFileAccessAuthorizer.h"],
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
":EndpointSecurityAPI",
|
||||
":EndpointSecurityEnrichedTypes",
|
||||
@@ -337,20 +366,28 @@ objc_library(
|
||||
":EndpointSecurityLogger",
|
||||
":EndpointSecurityMessage",
|
||||
":Metrics",
|
||||
":RateLimiter",
|
||||
":SNTDecisionCache",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
":TTYWriter",
|
||||
":WatchItemPolicy",
|
||||
":WatchItems",
|
||||
"//Source/common:Platform",
|
||||
"//Source/common:SNTBlockMessage",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTFileAccessEvent",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTStrengthify",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
"//Source/common:String",
|
||||
"@MOLCertificate",
|
||||
"@MOLCodesignChecker",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
"@com_google_absl//absl/container:flat_hash_set",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -366,8 +403,10 @@ objc_library(
|
||||
":Metrics",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTDeviceEvent",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -380,12 +419,24 @@ objc_library(
|
||||
":EndpointSecurityClient",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "RateLimiter",
|
||||
srcs = ["EventProviders/RateLimiter.mm"],
|
||||
hdrs = ["EventProviders/RateLimiter.h"],
|
||||
deps = [
|
||||
":Metrics",
|
||||
"//Source/common:BranchPrediction",
|
||||
"//Source/common:SystemResources",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "EndpointSecurityEnricher",
|
||||
srcs = ["EventProviders/EndpointSecurity/Enricher.mm"],
|
||||
@@ -427,12 +478,12 @@ objc_library(
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
":EndpointSecurityEnrichedTypes",
|
||||
":EndpointSecurityMessage",
|
||||
":SNTDecisionCache",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
"//Source/common:String",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -455,6 +506,7 @@ objc_library(
|
||||
],
|
||||
deps = [
|
||||
":EndpointSecurityMessage",
|
||||
"//Source/common:String",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -469,7 +521,6 @@ objc_library(
|
||||
":EndpointSecuritySerializerUtilities",
|
||||
":SNTDecisionCache",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
],
|
||||
@@ -488,7 +539,10 @@ objc_library(
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:String",
|
||||
"//Source/common:santa_cc_proto_library_wrapper",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_protobuf//src/google/protobuf/json",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -556,6 +610,7 @@ objc_library(
|
||||
":EndpointSecurityWriterNull",
|
||||
":EndpointSecurityWriterSpool",
|
||||
":EndpointSecurityWriterSyslog",
|
||||
":SNTDecisionCache",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
@@ -628,6 +683,7 @@ objc_library(
|
||||
hdrs = ["Metrics.h"],
|
||||
deps = [
|
||||
":SNTApplicationCoreMetrics",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTXPCMetricServiceInterface",
|
||||
@@ -656,10 +712,12 @@ objc_library(
|
||||
":SNTExecutionController",
|
||||
":SNTNotificationQueue",
|
||||
":SNTSyncdQueue",
|
||||
":TTYWriter",
|
||||
":WatchItems",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTFileAccessEvent",
|
||||
"//Source/common:SNTKVOManager",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTXPCNotifierInterface",
|
||||
@@ -681,11 +739,13 @@ objc_library(
|
||||
":Metrics",
|
||||
":SNTCompilerController",
|
||||
":SNTDatabaseController",
|
||||
":SNTDecisionCache",
|
||||
":SNTEventTable",
|
||||
":SNTExecutionController",
|
||||
":SNTNotificationQueue",
|
||||
":SNTRuleTable",
|
||||
":SNTSyncdQueue",
|
||||
":TTYWriter",
|
||||
":WatchItems",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTConfigurator",
|
||||
@@ -842,8 +902,10 @@ santa_unit_test(
|
||||
":Metrics",
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTDatabaseController",
|
||||
":SNTDecisionCache",
|
||||
":SNTEndpointSecurityAuthorizer",
|
||||
":SantadDeps",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:TestUtils",
|
||||
"@MOLCertificate",
|
||||
@@ -942,7 +1004,9 @@ santa_unit_test(
|
||||
"//Source/common:TestUtils",
|
||||
"//Source/common:santa_cc_proto_library_wrapper",
|
||||
"@OCMock",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_googletest//:gtest",
|
||||
"@com_google_protobuf//src/google/protobuf/json",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -962,6 +1026,16 @@ santa_unit_test(
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "RateLimiterTest",
|
||||
srcs = ["EventProviders/RateLimiterTest.mm"],
|
||||
deps = [
|
||||
":Metrics",
|
||||
":RateLimiter",
|
||||
"//Source/common:SystemResources",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "EndpointSecuritySerializerEmptyTest",
|
||||
srcs = ["Logs/EndpointSecurity/Serializers/EmptyTest.mm"],
|
||||
@@ -1181,6 +1255,7 @@ santa_unit_test(
|
||||
":WatchItems",
|
||||
"//Source/common:Platform",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:TestUtils",
|
||||
"@MOLCertificate",
|
||||
@@ -1228,6 +1303,7 @@ santa_unit_test(
|
||||
":SNTCompilerController",
|
||||
":SNTEndpointSecurityRecorder",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:TestUtils",
|
||||
"//Source/common:Unit",
|
||||
"@OCMock",
|
||||
@@ -1250,6 +1326,7 @@ santa_unit_test(
|
||||
":Metrics",
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTEndpointSecurityDeviceManager",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDeviceEvent",
|
||||
"//Source/common:TestUtils",
|
||||
@@ -1294,6 +1371,7 @@ test_suite(
|
||||
":EndpointSecurityWriterFileTest",
|
||||
":EndpointSecurityWriterSpoolTest",
|
||||
":MetricsTest",
|
||||
":RateLimiterTest",
|
||||
":SNTApplicationCoreMetricsTest",
|
||||
":SNTCompilerControllerTest",
|
||||
":SNTDecisionCacheTest",
|
||||
|
||||
@@ -39,9 +39,10 @@
|
||||
bail = YES;
|
||||
return;
|
||||
}
|
||||
[db close];
|
||||
[[NSFileManager defaultManager] removeItemAtPath:[db databasePath] error:NULL];
|
||||
[db open];
|
||||
[self closeDeleteReopenDatabase:db];
|
||||
} else if ([db userVersion] > [self currentSupportedVersion]) {
|
||||
LOGW(@"Database version newer than supported. Deleting.");
|
||||
[self closeDeleteReopenDatabase:db];
|
||||
}
|
||||
}];
|
||||
|
||||
@@ -58,11 +59,22 @@
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)closeDeleteReopenDatabase:(FMDatabase *)db {
|
||||
[db close];
|
||||
[[NSFileManager defaultManager] removeItemAtPath:[db databasePath] error:NULL];
|
||||
[db open];
|
||||
}
|
||||
|
||||
- (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (uint32_t)currentSupportedVersion {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Called at the end of initialization to ensure the table in the
|
||||
/// database exists and uses the latest schema.
|
||||
- (void)updateTableSchema {
|
||||
|
||||
@@ -18,8 +18,14 @@
|
||||
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
|
||||
static const uint32_t kEventTableCurrentVersion = 3;
|
||||
|
||||
@implementation SNTEventTable
|
||||
|
||||
- (uint32_t)currentSupportedVersion {
|
||||
return kEventTableCurrentVersion;
|
||||
}
|
||||
|
||||
- (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version {
|
||||
int newVersion = 0;
|
||||
|
||||
|
||||
@@ -57,10 +57,16 @@
|
||||
- (NSUInteger)teamIDRuleCount;
|
||||
|
||||
///
|
||||
/// @return Rule for binary or certificate with given SHA-256. The binary rule will be returned
|
||||
/// if it exists. If not, the certificate rule will be returned if it exists.
|
||||
/// @return Number of signing ID rules in the database
|
||||
///
|
||||
- (NSUInteger)signingIDRuleCount;
|
||||
|
||||
///
|
||||
/// @return Rule for 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;
|
||||
|
||||
@@ -69,11 +75,11 @@
|
||||
/// transaction will abort if any rule fails to add.
|
||||
///
|
||||
/// @param rules Array of SNTRule's to add.
|
||||
/// @param cleanSlate If true, remove all rules before adding the new rules.
|
||||
/// @param ruleCleanup Rule cleanup type to perform (e.g. all, none, non-transitive).
|
||||
/// @param error When returning NO, will be filled with appropriate error.
|
||||
/// @return YES if adding all rules passed, NO if any were rejected.
|
||||
///
|
||||
- (BOOL)addRules:(NSArray *)rules cleanSlate:(BOOL)cleanSlate error:(NSError **)error;
|
||||
- (BOOL)addRules:(NSArray *)rules ruleCleanup:(SNTRuleCleanup)cleanupType error:(NSError **)error;
|
||||
|
||||
///
|
||||
/// Checks the given array of rules to see if adding any of them to the rules database would
|
||||
@@ -95,6 +101,11 @@
|
||||
///
|
||||
- (void)removeOutdatedTransitiveRules;
|
||||
|
||||
///
|
||||
/// Retrieve all rules from the database for export.
|
||||
///
|
||||
- (NSArray<SNTRule *> *)retrieveAllRules;
|
||||
|
||||
///
|
||||
/// A map of a file hashes to cached decisions. This is used to pre-validate and whitelist
|
||||
/// certain critical system binaries that are integral to Santa's functionality.
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
|
||||
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;
|
||||
@@ -173,6 +175,10 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
self.criticalSystemBinaries = bins;
|
||||
}
|
||||
|
||||
- (uint32_t)currentSupportedVersion {
|
||||
return kRuleTableCurrentVersion;
|
||||
}
|
||||
|
||||
- (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version {
|
||||
// Lock this database from other processes
|
||||
[[db executeQuery:@"PRAGMA locking_mode = EXCLUSIVE;"] close];
|
||||
@@ -188,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;
|
||||
}
|
||||
@@ -204,12 +210,47 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
[db executeUpdate:@"ALTER TABLE 'rules' ADD 'timestamp' INTEGER"];
|
||||
newVersion = 3;
|
||||
}
|
||||
|
||||
if (version < 4) {
|
||||
// Rename `shasum` column to `identifier`.
|
||||
[db executeUpdate:@"ALTER TABLE 'rules' RENAME COLUMN 'shasum' TO 'identifier'"];
|
||||
newVersion = 4;
|
||||
}
|
||||
|
||||
if (version < 5) {
|
||||
// Migrate SNTRuleType enum values
|
||||
// Note: The reordering is intentional so that the type values are in order
|
||||
// of precedence.
|
||||
[db executeUpdate:@"UPDATE rules SET type = 1000 WHERE type = 1"];
|
||||
[db executeUpdate:@"UPDATE rules SET type = 3000 WHERE type = 2"];
|
||||
[db executeUpdate:@"UPDATE rules SET type = 4000 WHERE type = 3"];
|
||||
[db executeUpdate:@"UPDATE rules SET type = 2000 WHERE type = 4"];
|
||||
|
||||
newVersion = 5;
|
||||
}
|
||||
|
||||
if (version < 6) {
|
||||
// Force hash identifiers for Binary and Certificate rules to always be lowercase
|
||||
[db executeUpdate:@"UPDATE rules SET identifier = LOWER(identifier) WHERE type = ? OR type = ?",
|
||||
@(SNTRuleTypeBinary), @(SNTRuleTypeCertificate)];
|
||||
|
||||
// Force team ID identifiers for TeamID rules to always be uppercase
|
||||
[db executeUpdate:@"UPDATE rules SET identifier = UPPER(identifier) WHERE type = ?",
|
||||
@(SNTRuleTypeTeamID)];
|
||||
|
||||
// Note: Intentionally not attempting to migrate exsting SigningID rules to enforce
|
||||
// the TeamID component to be uppercase. Since this is a newer rule type, it is
|
||||
// assumed to be unnecessary and we'd rather not maintain the SQL to perform this
|
||||
// migration automatically.
|
||||
|
||||
newVersion = 6;
|
||||
}
|
||||
|
||||
if (version < 7) {
|
||||
[db executeUpdate:@"ALTER TABLE 'rules' ADD 'customurl' TEXT"];
|
||||
newVersion = 7;
|
||||
}
|
||||
|
||||
// Save signing info for launchd and santad. Used to ensure they are always allowed.
|
||||
self.santadCSInfo = [[MOLCodesignChecker alloc] initWithSelf];
|
||||
self.launchdCSInfo = [[MOLCodesignChecker alloc] initWithPID:1];
|
||||
@@ -230,20 +271,20 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSUInteger)binaryRuleCount {
|
||||
- (NSUInteger)ruleCountForRuleType:(SNTRuleType)ruleType {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=1"];
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=?", @(ruleType)];
|
||||
}];
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSUInteger)binaryRuleCount {
|
||||
return [self ruleCountForRuleType:SNTRuleTypeBinary];
|
||||
}
|
||||
|
||||
- (NSUInteger)certificateRuleCount {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=2"];
|
||||
}];
|
||||
return count;
|
||||
return [self ruleCountForRuleType:SNTRuleTypeCertificate];
|
||||
}
|
||||
|
||||
- (NSUInteger)compilerRuleCount {
|
||||
@@ -265,22 +306,25 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
}
|
||||
|
||||
- (NSUInteger)teamIDRuleCount {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=3"];
|
||||
}];
|
||||
return count;
|
||||
return [self ruleCountForRuleType:SNTRuleTypeTeamID];
|
||||
}
|
||||
|
||||
- (NSUInteger)signingIDRuleCount {
|
||||
return [self ruleCountForRuleType:SNTRuleTypeSigningID];
|
||||
}
|
||||
|
||||
- (SNTRule *)ruleFromResultSet:(FMResultSet *)rs {
|
||||
return [[SNTRule alloc] initWithIdentifier:[rs stringForColumn:@"identifier"]
|
||||
state:[rs intForColumn:@"state"]
|
||||
type:[rs intForColumn:@"type"]
|
||||
customMsg:[rs stringForColumn:@"custommsg"]
|
||||
timestamp:[rs intForColumn:@"timestamp"]];
|
||||
SNTRule *r = [[SNTRule alloc] initWithIdentifier:[rs stringForColumn:@"identifier"]
|
||||
state:[rs intForColumn:@"state"]
|
||||
type:[rs intForColumn:@"type"]
|
||||
customMsg:[rs stringForColumn:@"custommsg"]
|
||||
timestamp:[rs intForColumn:@"timestamp"]];
|
||||
r.customURL = [rs stringForColumn:@"customurl"];
|
||||
return r;
|
||||
}
|
||||
|
||||
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
|
||||
signingID:(NSString *)signingID
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID {
|
||||
__block SNTRule *rule;
|
||||
@@ -288,12 +332,27 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
// Look for a static rule that matches.
|
||||
NSDictionary *staticRules = [[SNTConfigurator configurator] staticRules];
|
||||
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];
|
||||
if (rule.type == SNTRuleTypeBinary) return rule;
|
||||
if (rule.type == SNTRuleTypeBinary) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[signingID];
|
||||
if (rule.type == SNTRuleTypeSigningID) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[certificateSHA256];
|
||||
if (rule.type == SNTRuleTypeCertificate) return rule;
|
||||
if (rule.type == SNTRuleTypeCertificate) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[teamID];
|
||||
if (rule.type == SNTRuleTypeTeamID) return rule;
|
||||
if (rule.type == SNTRuleTypeTeamID) {
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
|
||||
// Now query the database.
|
||||
@@ -301,7 +360,7 @@ 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 > Certificates > Team IDs.
|
||||
// The intended order of precedence is Binaries > Signing IDs > Certificates > Team IDs.
|
||||
//
|
||||
// As such the query should have "ORDER BY type DESC" before the LIMIT, to ensure that is the
|
||||
// case. However, in all tested versions of SQLite that ORDER BY clause is unnecessary: the query
|
||||
@@ -316,10 +375,12 @@ 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=1) OR "
|
||||
@"(identifier=? AND type=2) OR (identifier=? AND type=3) LIMIT 1",
|
||||
binarySHA256, certificateSHA256, teamID];
|
||||
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];
|
||||
if ([rs next]) {
|
||||
rule = [self ruleFromResultSet:rs];
|
||||
}
|
||||
@@ -342,7 +403,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
#pragma mark Adding
|
||||
|
||||
- (BOOL)addRules:(NSArray *)rules
|
||||
cleanSlate:(BOOL)cleanSlate
|
||||
ruleCleanup:(SNTRuleCleanup)cleanupType
|
||||
error:(NSError *__autoreleasing *)error {
|
||||
if (!rules || rules.count < 1) {
|
||||
[self fillError:error code:SNTRuleTableErrorEmptyRuleArray message:nil];
|
||||
@@ -352,8 +413,10 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
__block BOOL failed = NO;
|
||||
|
||||
[self inTransaction:^(FMDatabase *db, BOOL *rollback) {
|
||||
if (cleanSlate) {
|
||||
if (cleanupType == SNTRuleCleanupAll) {
|
||||
[db executeUpdate:@"DELETE FROM rules"];
|
||||
} else if (cleanupType == SNTRuleCleanupNonTransitive) {
|
||||
[db executeUpdate:@"DELETE FROM rules WHERE state != ?", @(SNTRuleStateAllowTransitive)];
|
||||
}
|
||||
|
||||
for (SNTRule *rule in rules) {
|
||||
@@ -373,10 +436,10 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
}
|
||||
} else {
|
||||
if (![db executeUpdate:@"INSERT OR REPLACE INTO rules "
|
||||
@"(identifier, state, type, custommsg, timestamp) "
|
||||
@"VALUES (?, ?, ?, ?, ?);",
|
||||
@"(identifier, state, type, custommsg, customurl, timestamp) "
|
||||
@"VALUES (?, ?, ?, ?, ?, ?);",
|
||||
rule.identifier, @(rule.state), @(rule.type), rule.customMsg,
|
||||
@(rule.timestamp)]) {
|
||||
rule.customURL, @(rule.timestamp)]) {
|
||||
[self fillError:error
|
||||
code:SNTRuleTableErrorInsertOrReplaceFailed
|
||||
message:[db lastErrorMessage]];
|
||||
@@ -478,4 +541,19 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark Querying
|
||||
|
||||
// Retrieve all rules from the Database
|
||||
- (NSArray<SNTRule *> *)retrieveAllRules {
|
||||
NSMutableArray<SNTRule *> *rules = [NSMutableArray array];
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
FMResultSet *rs = [db executeQuery:@"SELECT * FROM rules"];
|
||||
while ([rs next]) {
|
||||
[rules addObject:[self ruleFromResultSet:rs]];
|
||||
}
|
||||
[rs close];
|
||||
}];
|
||||
return rules;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -36,25 +36,47 @@
|
||||
|
||||
- (SNTRule *)_exampleTeamIDRule {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
r.identifier = @"teamID";
|
||||
r.identifier = @"ABCDEFGHIJ";
|
||||
r.state = SNTRuleStateBlock;
|
||||
r.type = SNTRuleTypeTeamID;
|
||||
r.customMsg = @"A teamID rule";
|
||||
return r;
|
||||
}
|
||||
|
||||
- (SNTRule *)_exampleSigningIDRuleIsPlatform:(BOOL)isPlatformBinary {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
if (isPlatformBinary) {
|
||||
r.identifier = @"platform:signingID";
|
||||
} else {
|
||||
r.identifier = @"ABCDEFGHIJ:signingID";
|
||||
}
|
||||
r.state = SNTRuleStateBlock;
|
||||
r.type = SNTRuleTypeSigningID;
|
||||
r.customMsg = @"A signingID rule";
|
||||
return r;
|
||||
}
|
||||
|
||||
- (SNTRule *)_exampleBinaryRule {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
r.identifier = @"a";
|
||||
r.identifier = @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670";
|
||||
r.state = SNTRuleStateBlock;
|
||||
r.type = SNTRuleTypeBinary;
|
||||
r.customMsg = @"A rule";
|
||||
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 = @"b";
|
||||
r.identifier = @"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258";
|
||||
r.state = SNTRuleStateAllow;
|
||||
r.type = SNTRuleTypeCertificate;
|
||||
return r;
|
||||
@@ -65,7 +87,7 @@
|
||||
NSUInteger binaryRuleCount = self.sut.binaryRuleCount;
|
||||
|
||||
NSError *error;
|
||||
[self.sut addRules:@[ [self _exampleBinaryRule] ] cleanSlate:NO error:&error];
|
||||
[self.sut addRules:@[ [self _exampleBinaryRule] ] ruleCleanup:SNTRuleCleanupNone error:&error];
|
||||
|
||||
XCTAssertEqual(self.sut.ruleCount, ruleCount + 1);
|
||||
XCTAssertEqual(self.sut.binaryRuleCount, binaryRuleCount + 1);
|
||||
@@ -75,24 +97,49 @@
|
||||
- (void)testAddRulesClean {
|
||||
// Add a binary rule without clean slate
|
||||
NSError *error = nil;
|
||||
XCTAssertTrue([self.sut addRules:@[ [self _exampleBinaryRule] ] cleanSlate:NO error:&error]);
|
||||
XCTAssertTrue([self.sut addRules:@[ [self _exampleBinaryRule] ]
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:&error]);
|
||||
XCTAssertNil(error);
|
||||
|
||||
// Now add a cert rule with a clean slate, assert that the binary rule was removed
|
||||
error = nil;
|
||||
XCTAssertTrue(([self.sut addRules:@[ [self _exampleCertRule] ] cleanSlate:YES error:&error]));
|
||||
XCTAssertTrue(([self.sut addRules:@[ [self _exampleCertRule] ]
|
||||
ruleCleanup:SNTRuleCleanupAll
|
||||
error:&error]));
|
||||
XCTAssertEqual([self.sut binaryRuleCount], 0);
|
||||
XCTAssertNil(error);
|
||||
}
|
||||
|
||||
- (void)testAddRulesCleanNonTransitive {
|
||||
// Add a multiple binary rules, including a transitive rule
|
||||
NSError *error = nil;
|
||||
XCTAssertTrue(([self.sut addRules:@[
|
||||
[self _exampleBinaryRule], [self _exampleCertRule], [self _exampleTransitiveRule]
|
||||
]
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:&error]));
|
||||
XCTAssertEqual([self.sut binaryRuleCount], 2);
|
||||
XCTAssertNil(error);
|
||||
|
||||
// Now add a cert rule while cleaning non-transitive rules. Ensure the transitive rule remains
|
||||
error = nil;
|
||||
XCTAssertTrue(([self.sut addRules:@[ [self _exampleCertRule] ]
|
||||
ruleCleanup:SNTRuleCleanupNonTransitive
|
||||
error:&error]));
|
||||
XCTAssertEqual([self.sut binaryRuleCount], 1);
|
||||
XCTAssertEqual([self.sut certificateRuleCount], 1);
|
||||
XCTAssertNil(error);
|
||||
}
|
||||
|
||||
- (void)testAddMultipleRules {
|
||||
NSUInteger ruleCount = self.sut.ruleCount;
|
||||
|
||||
NSError *error;
|
||||
[self.sut
|
||||
addRules:@[ [self _exampleBinaryRule], [self _exampleCertRule], [self _exampleBinaryRule] ]
|
||||
cleanSlate:NO
|
||||
error:&error];
|
||||
addRules:@[ [self _exampleBinaryRule], [self _exampleCertRule], [self _exampleBinaryRule] ]
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(self.sut.ruleCount, ruleCount + 2);
|
||||
XCTAssertNil(error);
|
||||
@@ -100,91 +147,179 @@
|
||||
|
||||
- (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);
|
||||
}
|
||||
|
||||
- (void)testAddInvalidRule {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
r.identifier = @"a";
|
||||
r.identifier = @"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258";
|
||||
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:@"a" certificateSHA256:nil teamID:nil];
|
||||
SNTRule *r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"a");
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeBinary);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"b" certificateSHA256:nil teamID:nil];
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"b6ee1c3c5a715c049d14a8457faa6b6701b8507efe908300e238e0768bd759c2"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
- (void)testFetchCertificateRule {
|
||||
[self.sut addRules:@[ [self _exampleBinaryRule], [self _exampleCertRule] ]
|
||||
cleanSlate:NO
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:nil];
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil certificateSHA256:@"b" teamID:nil];
|
||||
SNTRule *r = [self.sut
|
||||
ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:nil];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"b");
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCertificate);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil certificateSHA256:@"a" teamID:nil];
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:@"5bdab1288fc16892fef50c658db54f1e2e19cf8f71cc55f77de2b95e051e2562"
|
||||
teamID:nil];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
- (void)testFetchTeamIDRule {
|
||||
[self.sut addRules:@[ [self _exampleBinaryRule], [self _exampleTeamIDRule] ]
|
||||
cleanSlate:NO
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:nil];
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil certificateSHA256:nil teamID:@"teamID"];
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID");
|
||||
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeTeamID);
|
||||
XCTAssertEqual([self.sut teamIDRuleCount], 1);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil certificateSHA256:nil teamID:@"nonexistentTeamID"];
|
||||
r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@"nonexistentTeamID"];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
- (void)testFetchSigningIDRule {
|
||||
[self.sut addRules:@[
|
||||
[self _exampleBinaryRule], [self _exampleSigningIDRuleIsPlatform:YES],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO]
|
||||
]
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:nil];
|
||||
|
||||
XCTAssertEqual([self.sut signingIDRuleCount], 2);
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:@"ABCDEFGHIJ:signingID"
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:@"platform:signingID"
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"platform:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil signingID:@"nonexistent" certificateSHA256:nil teamID:nil];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
- (void)testFetchRuleOrdering {
|
||||
[self.sut
|
||||
addRules:@[ [self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule] ]
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
[self.sut addRules:@[
|
||||
[self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO]
|
||||
]
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:nil];
|
||||
|
||||
// This test verifies that the implicit rule ordering we've been abusing is still working.
|
||||
// See the comment in SNTRuleTable#ruleForBinarySHA256:certificateSHA256:teamID
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:@"a" certificateSHA256:@"b" teamID:@"teamID"];
|
||||
SNTRule *r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:@"ABCDEFGHIJ:signingID"
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"a");
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeBinary, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"a" certificateSHA256:@"unknowncert" teamID:@"teamID"];
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:@"ABCDEFGHIJ:signingID"
|
||||
certificateSHA256:@"unknowncert"
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"a");
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeBinary, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"unknown" certificateSHA256:@"b" teamID:@"teamID"];
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"unknown"
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"b");
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCertificate, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"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"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeTeamID, @"Implicit rule ordering failed (TeamID)");
|
||||
}
|
||||
|
||||
- (void)testBadDatabase {
|
||||
@@ -194,10 +329,31 @@
|
||||
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];
|
||||
}
|
||||
|
||||
- (void)testRetrieveAllRulesWithEmptyDatabase {
|
||||
NSArray<SNTRule *> *rules = [self.sut retrieveAllRules];
|
||||
XCTAssertEqual(rules.count, 0);
|
||||
}
|
||||
|
||||
- (void)testRetrieveAllRulesWithMultipleRules {
|
||||
[self.sut addRules:@[
|
||||
[self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO]
|
||||
]
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:nil];
|
||||
|
||||
NSArray<SNTRule *> *rules = [self.sut retrieveAllRules];
|
||||
XCTAssertEqual(rules.count, 4);
|
||||
XCTAssertEqualObjects(rules[0], [self _exampleCertRule]);
|
||||
XCTAssertEqualObjects(rules[1], [self _exampleBinaryRule]);
|
||||
XCTAssertEqualObjects(rules[2], [self _exampleTeamIDRule]);
|
||||
XCTAssertEqualObjects(rules[3], [self _exampleSigningIDRuleIsPlatform:NO]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#ifndef SANTA__SANTAD__DATALAYER_WATCHITEMPOLICY_H
|
||||
#define SANTA__SANTAD__DATALAYER_WATCHITEMPOLICY_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
|
||||
#include <optional>
|
||||
@@ -29,15 +30,17 @@ enum class WatchItemPathType {
|
||||
kLiteral,
|
||||
};
|
||||
|
||||
static constexpr WatchItemPathType kWatchItemPolicyDefaultPathType =
|
||||
WatchItemPathType::kLiteral;
|
||||
static constexpr WatchItemPathType kWatchItemPolicyDefaultPathType = WatchItemPathType::kLiteral;
|
||||
static constexpr bool kWatchItemPolicyDefaultAllowReadAccess = false;
|
||||
static constexpr bool kWatchItemPolicyDefaultAuditOnly = true;
|
||||
static constexpr bool kWatchItemPolicyDefaultInvertProcessExceptions = false;
|
||||
static constexpr bool kWatchItemPolicyDefaultEnableSilentMode = false;
|
||||
static constexpr bool kWatchItemPolicyDefaultEnableSilentTTYMode = false;
|
||||
|
||||
struct WatchItemPolicy {
|
||||
struct Process {
|
||||
Process(std::string bp, std::string sid, std::string ti,
|
||||
std::vector<uint8_t> cdh, std::string ch, std::optional<bool> pb)
|
||||
Process(std::string bp, std::string sid, std::string ti, std::vector<uint8_t> cdh,
|
||||
std::string ch, std::optional<bool> pb)
|
||||
: binary_path(bp),
|
||||
signing_id(sid),
|
||||
team_id(ti),
|
||||
@@ -46,13 +49,11 @@ struct WatchItemPolicy {
|
||||
platform_binary(pb) {}
|
||||
|
||||
bool operator==(const Process &other) const {
|
||||
return binary_path == other.binary_path &&
|
||||
signing_id == other.signing_id && team_id == other.team_id &&
|
||||
cdhash == other.cdhash &&
|
||||
return binary_path == other.binary_path && signing_id == other.signing_id &&
|
||||
team_id == other.team_id && cdhash == other.cdhash &&
|
||||
certificate_sha256 == other.certificate_sha256 &&
|
||||
platform_binary.has_value() == other.platform_binary.has_value() &&
|
||||
platform_binary.value_or(false) ==
|
||||
other.platform_binary.value_or(false);
|
||||
platform_binary.value_or(false) == other.platform_binary.value_or(false);
|
||||
}
|
||||
|
||||
bool operator!=(const Process &other) const { return !(*this == other); }
|
||||
@@ -69,31 +70,51 @@ struct WatchItemPolicy {
|
||||
WatchItemPathType pt = kWatchItemPolicyDefaultPathType,
|
||||
bool ara = kWatchItemPolicyDefaultAllowReadAccess,
|
||||
bool ao = kWatchItemPolicyDefaultAuditOnly,
|
||||
std::vector<Process> procs = {})
|
||||
bool ipe = kWatchItemPolicyDefaultInvertProcessExceptions,
|
||||
bool esm = kWatchItemPolicyDefaultEnableSilentMode,
|
||||
bool estm = kWatchItemPolicyDefaultEnableSilentTTYMode, std::string_view cm = "",
|
||||
NSString *edu = nil, NSString *edt = nil, std::vector<Process> procs = {})
|
||||
: name(n),
|
||||
path(p),
|
||||
path_type(pt),
|
||||
allow_read_access(ara),
|
||||
audit_only(ao),
|
||||
invert_process_exceptions(ipe),
|
||||
silent(esm),
|
||||
silent_tty(estm),
|
||||
custom_message(cm.length() == 0 ? std::nullopt : std::make_optional<std::string>(cm)),
|
||||
// Note: Empty string considered valid for event_detail_url to allow rules
|
||||
// overriding global setting in order to hide the button.
|
||||
event_detail_url(edu == nil ? std::nullopt : std::make_optional<NSString *>(edu)),
|
||||
event_detail_text(edt.length == 0 ? std::nullopt : std::make_optional<NSString *>(edt)),
|
||||
processes(std::move(procs)) {}
|
||||
|
||||
bool operator==(const WatchItemPolicy &other) const {
|
||||
return name == other.name && path == other.path &&
|
||||
path_type == other.path_type &&
|
||||
allow_read_access == other.allow_read_access &&
|
||||
audit_only == other.audit_only && processes == other.processes;
|
||||
// Note: custom_message, event_detail_url, and event_detail_text are not currently considered
|
||||
// for equality purposes
|
||||
return name == other.name && path == other.path && path_type == other.path_type &&
|
||||
allow_read_access == other.allow_read_access && audit_only == other.audit_only &&
|
||||
invert_process_exceptions == other.invert_process_exceptions && silent == other.silent &&
|
||||
silent_tty == other.silent_tty && processes == other.processes;
|
||||
}
|
||||
|
||||
bool operator!=(const WatchItemPolicy &other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
bool operator!=(const WatchItemPolicy &other) const { return !(*this == other); }
|
||||
|
||||
std::string name;
|
||||
std::string path;
|
||||
WatchItemPathType path_type;
|
||||
bool allow_read_access;
|
||||
bool audit_only;
|
||||
bool invert_process_exceptions;
|
||||
bool silent;
|
||||
bool silent_tty;
|
||||
std::optional<std::string> custom_message;
|
||||
std::optional<NSString *> event_detail_url;
|
||||
std::optional<NSString *> event_detail_text;
|
||||
std::vector<Process> processes;
|
||||
|
||||
// WIP - No current way to control via config
|
||||
std::string version = "temp_version";
|
||||
};
|
||||
|
||||
} // namespace santa::santad::data_layer
|
||||
|
||||
@@ -39,6 +39,10 @@ extern NSString *const kWatchItemConfigKeyPathsIsPrefix;
|
||||
extern NSString *const kWatchItemConfigKeyOptions;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsAllowReadAccess;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsAuditOnly;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsInvertProcessExceptions;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsEnableSilentMode;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsEnableSilentTTYMode;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsCustomMessage;
|
||||
extern NSString *const kWatchItemConfigKeyProcesses;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesBinaryPath;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesCertificateSha256;
|
||||
@@ -70,9 +74,15 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
|
||||
// Factory
|
||||
static std::shared_ptr<WatchItems> Create(NSString *config_path,
|
||||
uint64_t reapply_config_frequency_secs);
|
||||
// Factory
|
||||
static std::shared_ptr<WatchItems> Create(NSDictionary *config,
|
||||
uint64_t reapply_config_frequency_secs);
|
||||
|
||||
WatchItems(NSString *config_path_, dispatch_queue_t q, dispatch_source_t timer_source,
|
||||
WatchItems(NSString *config_path, dispatch_queue_t q, dispatch_source_t timer_source,
|
||||
void (^periodic_task_complete_f)(void) = nullptr);
|
||||
WatchItems(NSDictionary *config, dispatch_queue_t q, dispatch_source_t timer_source,
|
||||
void (^periodic_task_complete_f)(void) = nullptr);
|
||||
|
||||
~WatchItems();
|
||||
|
||||
void BeginPeriodicTask();
|
||||
@@ -80,13 +90,21 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
|
||||
void RegisterClient(id<SNTEndpointSecurityDynamicEventHandler> client);
|
||||
|
||||
void SetConfigPath(NSString *config_path);
|
||||
void SetConfig(NSDictionary *config);
|
||||
|
||||
VersionAndPolicies FindPolciesForPaths(const std::vector<std::string_view> &paths);
|
||||
|
||||
std::optional<WatchItemsState> State();
|
||||
|
||||
std::pair<NSString *, NSString *> EventDetailLinkInfo(
|
||||
const std::shared_ptr<WatchItemPolicy> &watch_item);
|
||||
|
||||
friend class santa::santad::data_layer::WatchItemsPeer;
|
||||
|
||||
private:
|
||||
static std::shared_ptr<WatchItems> CreateInternal(NSString *config_path, NSDictionary *config,
|
||||
uint64_t reapply_config_frequency_secs);
|
||||
|
||||
NSDictionary *ReadConfig();
|
||||
NSDictionary *ReadConfigLocked() ABSL_SHARED_LOCKS_REQUIRED(lock_);
|
||||
void ReloadConfig(NSDictionary *new_config);
|
||||
@@ -98,6 +116,7 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
|
||||
std::set<std::pair<std::string, WatchItemPathType>> &paths);
|
||||
|
||||
NSString *config_path_;
|
||||
NSDictionary *embedded_config_;
|
||||
dispatch_queue_t q_;
|
||||
dispatch_source_t timer_source_;
|
||||
void (^periodic_task_complete_f_)(void);
|
||||
@@ -112,6 +131,8 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
|
||||
std::string policy_version_ ABSL_GUARDED_BY(lock_);
|
||||
std::set<id<SNTEndpointSecurityDynamicEventHandler>> registerd_clients_ ABSL_GUARDED_BY(lock_);
|
||||
bool periodic_task_started_ = false;
|
||||
NSString *policy_event_detail_url_ ABSL_GUARDED_BY(lock_);
|
||||
NSString *policy_event_detail_text_ ABSL_GUARDED_BY(lock_);
|
||||
};
|
||||
|
||||
} // namespace santa::santad::data_layer
|
||||
|
||||
@@ -34,14 +34,20 @@
|
||||
|
||||
#import "Source/common/PrefixTree.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/String.h"
|
||||
#import "Source/common/Unit.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
|
||||
using santa::common::NSStringToUTF8String;
|
||||
using santa::common::NSStringToUTF8StringView;
|
||||
using santa::common::PrefixTree;
|
||||
using santa::common::Unit;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
using santa::santad::data_layer::WatchItemPolicy;
|
||||
|
||||
NSString *const kWatchItemConfigKeyVersion = @"Version";
|
||||
NSString *const kWatchItemConfigKeyEventDetailURL = @"EventDetailURL";
|
||||
NSString *const kWatchItemConfigKeyEventDetailText = @"EventDetailText";
|
||||
NSString *const kWatchItemConfigKeyWatchItems = @"WatchItems";
|
||||
NSString *const kWatchItemConfigKeyPaths = @"Paths";
|
||||
NSString *const kWatchItemConfigKeyPathsPath = @"Path";
|
||||
@@ -49,6 +55,12 @@ NSString *const kWatchItemConfigKeyPathsIsPrefix = @"IsPrefix";
|
||||
NSString *const kWatchItemConfigKeyOptions = @"Options";
|
||||
NSString *const kWatchItemConfigKeyOptionsAllowReadAccess = @"AllowReadAccess";
|
||||
NSString *const kWatchItemConfigKeyOptionsAuditOnly = @"AuditOnly";
|
||||
NSString *const kWatchItemConfigKeyOptionsInvertProcessExceptions = @"InvertProcessExceptions";
|
||||
NSString *const kWatchItemConfigKeyOptionsEnableSilentMode = @"EnableSilentMode";
|
||||
NSString *const kWatchItemConfigKeyOptionsEnableSilentTTYMode = @"EnableSilentTTYMode";
|
||||
NSString *const kWatchItemConfigKeyOptionsCustomMessage = @"BlockMessage";
|
||||
NSString *const kWatchItemConfigKeyOptionsEventDetailURL = kWatchItemConfigKeyEventDetailURL;
|
||||
NSString *const kWatchItemConfigKeyOptionsEventDetailText = kWatchItemConfigKeyEventDetailText;
|
||||
NSString *const kWatchItemConfigKeyProcesses = @"Processes";
|
||||
NSString *const kWatchItemConfigKeyProcessesBinaryPath = @"BinaryPath";
|
||||
NSString *const kWatchItemConfigKeyProcessesCertificateSha256 = @"CertificateSha256";
|
||||
@@ -68,6 +80,22 @@ static constexpr NSUInteger kMaxSigningIDLength = 512;
|
||||
// churn rebuilding glob paths based on the state of the filesystem.
|
||||
static constexpr uint64_t kMinReapplyConfigFrequencySecs = 15;
|
||||
|
||||
// Semi-arbitrary max custom message length. The goal is to protect against
|
||||
// potential unbounded lengths, but no real reason this cannot be higher.
|
||||
static constexpr NSUInteger kWatchItemConfigOptionCustomMessageMaxLength = 2048;
|
||||
|
||||
// Semi-arbitrary max event detail text length. The text has to fit on a button
|
||||
// and shouldn't be too large.
|
||||
static constexpr NSUInteger kWatchItemConfigEventDetailTextMaxLength = 48;
|
||||
|
||||
// Servers are recommended to support up to 8000 octets.
|
||||
// https://www.rfc-editor.org/rfc/rfc9110#section-4.1-5
|
||||
//
|
||||
// Seems excessive but no good reason to not allow long URLs. However because
|
||||
// the URL supports pseudo-format strings that can extend the length, a smaller
|
||||
// max is used here.
|
||||
static constexpr NSUInteger kWatchItemConfigEventDetailURLMaxLength = 6000;
|
||||
|
||||
namespace santa::santad::data_layer {
|
||||
|
||||
// Type aliases
|
||||
@@ -121,6 +149,10 @@ static std::vector<uint8_t> HexStringToBytes(NSString *str) {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static inline bool GetBoolValue(NSDictionary *options, NSString *key, bool default_value) {
|
||||
return options[key] ? [options[key] boolValue] : default_value;
|
||||
}
|
||||
|
||||
// Given a length, returns a ValidatorBlock that confirms the
|
||||
// string is a valid hex string of the given length.
|
||||
ValidatorBlock HexValidator(NSUInteger expected_length) {
|
||||
@@ -135,8 +167,8 @@ ValidatorBlock HexValidator(NSUInteger expected_length) {
|
||||
};
|
||||
}
|
||||
|
||||
// Given a max length, returns a ValidatorBlock that confirms the
|
||||
// string is a not longer than the max.
|
||||
// Given a min and max length, returns a ValidatorBlock that confirms the
|
||||
// string is within the given bounds.
|
||||
ValidatorBlock LenRangeValidator(NSUInteger min_length, NSUInteger max_length) {
|
||||
return ^bool(NSString *val, NSError **err) {
|
||||
if (val.length < min_length) {
|
||||
@@ -255,7 +287,7 @@ std::variant<Unit, PathList> VerifyConfigWatchItemPaths(NSArray<id> *paths, NSEr
|
||||
return Unit{};
|
||||
}
|
||||
|
||||
path_list.push_back({std::string(path_str.UTF8String, path_str.length), path_type});
|
||||
path_list.push_back({NSStringToUTF8String(path_str), path_type});
|
||||
} else if ([path isKindOfClass:[NSString class]]) {
|
||||
if (!LenRangeValidator(1, PATH_MAX)(path, err)) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Invalid path length: %@",
|
||||
@@ -264,8 +296,8 @@ std::variant<Unit, PathList> VerifyConfigWatchItemPaths(NSArray<id> *paths, NSEr
|
||||
return Unit{};
|
||||
}
|
||||
|
||||
path_list.push_back({std::string(((NSString *)path).UTF8String, ((NSString *)path).length),
|
||||
kWatchItemPolicyDefaultPathType});
|
||||
path_list.push_back(
|
||||
{NSStringToUTF8String(((NSString *)path)), kWatchItemPolicyDefaultPathType});
|
||||
} else {
|
||||
PopulateError(
|
||||
err, [NSString stringWithFormat:
|
||||
@@ -340,12 +372,11 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
|
||||
}
|
||||
|
||||
proc_list.push_back(WatchItemPolicy::Process(
|
||||
std::string([(process[kWatchItemConfigKeyProcessesBinaryPath] ?: @"") UTF8String]),
|
||||
std::string([(process[kWatchItemConfigKeyProcessesSigningID] ?: @"") UTF8String]),
|
||||
std::string([(process[kWatchItemConfigKeyProcessesTeamID] ?: @"") UTF8String]),
|
||||
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesBinaryPath] ?: @""),
|
||||
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesSigningID] ?: @""),
|
||||
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesTeamID] ?: @""),
|
||||
HexStringToBytes(process[kWatchItemConfigKeyProcessesCDHash]),
|
||||
std::string(
|
||||
[(process[kWatchItemConfigKeyProcessesCertificateSha256] ?: @"") UTF8String]),
|
||||
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesCertificateSha256] ?: @""),
|
||||
process[kWatchItemConfigKeyProcessesPlatformBinary]
|
||||
? std::make_optional(
|
||||
(bool)[process[kWatchItemConfigKeyProcessesPlatformBinary] boolValue])
|
||||
@@ -373,6 +404,18 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
|
||||
/// <false/>
|
||||
/// <key>AuditOnly</key>
|
||||
/// <false/>
|
||||
/// <key>InvertProcessExceptions</key>
|
||||
/// <false/>
|
||||
/// <key>EnableSilentMode</key>
|
||||
/// <true/>
|
||||
/// <key>EnableSilentTTYMode</key>
|
||||
/// <true/>
|
||||
/// <key>BlockMessage</key>
|
||||
/// <string>...</string>
|
||||
/// <key>EventDetailURL</key>
|
||||
/// <string>...</string>
|
||||
/// <key>EventDetailText</key>
|
||||
/// <string>...</string>
|
||||
/// </dict>
|
||||
/// <key>Processes</key>
|
||||
/// <array>
|
||||
@@ -399,22 +442,48 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
|
||||
NSDictionary *options = watch_item[kWatchItemConfigKeyOptions];
|
||||
if (options) {
|
||||
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsAllowReadAccess, [NSNumber class],
|
||||
err)) {
|
||||
NSArray<NSString *> *boolOptions = @[
|
||||
kWatchItemConfigKeyOptionsAllowReadAccess,
|
||||
kWatchItemConfigKeyOptionsAuditOnly,
|
||||
kWatchItemConfigKeyOptionsInvertProcessExceptions,
|
||||
kWatchItemConfigKeyOptionsEnableSilentMode,
|
||||
kWatchItemConfigKeyOptionsEnableSilentTTYMode,
|
||||
];
|
||||
|
||||
for (NSString *key in boolOptions) {
|
||||
if (!VerifyConfigKey(options, key, [NSNumber class], err)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsCustomMessage, [NSString class], err,
|
||||
false,
|
||||
LenRangeValidator(0, kWatchItemConfigOptionCustomMessageMaxLength))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsAuditOnly, [NSNumber class], err)) {
|
||||
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsEventDetailURL, [NSString class], err,
|
||||
false, LenRangeValidator(0, kWatchItemConfigEventDetailURLMaxLength))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsEventDetailText, [NSString class], err,
|
||||
false, LenRangeValidator(0, kWatchItemConfigEventDetailTextMaxLength))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool allow_read_access = options[kWatchItemConfigKeyOptionsAllowReadAccess]
|
||||
? [options[kWatchItemConfigKeyOptionsAllowReadAccess] boolValue]
|
||||
: kWatchItemPolicyDefaultAllowReadAccess;
|
||||
bool audit_only = options[kWatchItemConfigKeyOptionsAuditOnly]
|
||||
? [options[kWatchItemConfigKeyOptionsAuditOnly] boolValue]
|
||||
: kWatchItemPolicyDefaultAuditOnly;
|
||||
bool allow_read_access = GetBoolValue(options, kWatchItemConfigKeyOptionsAllowReadAccess,
|
||||
kWatchItemPolicyDefaultAllowReadAccess);
|
||||
bool audit_only =
|
||||
GetBoolValue(options, kWatchItemConfigKeyOptionsAuditOnly, kWatchItemPolicyDefaultAuditOnly);
|
||||
bool invert_process_exceptions =
|
||||
GetBoolValue(options, kWatchItemConfigKeyOptionsInvertProcessExceptions,
|
||||
kWatchItemPolicyDefaultInvertProcessExceptions);
|
||||
bool enable_silent_mode = GetBoolValue(options, kWatchItemConfigKeyOptionsEnableSilentMode,
|
||||
kWatchItemPolicyDefaultEnableSilentMode);
|
||||
bool enable_silent_tty_mode = GetBoolValue(options, kWatchItemConfigKeyOptionsEnableSilentTTYMode,
|
||||
kWatchItemPolicyDefaultEnableSilentTTYMode);
|
||||
|
||||
std::variant<Unit, ProcessList> proc_list = VerifyConfigWatchItemProcesses(watch_item, err);
|
||||
if (std::holds_alternative<Unit>(proc_list)) {
|
||||
@@ -423,8 +492,12 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
|
||||
for (const PathAndTypePair &path_type_pair : std::get<PathList>(path_list)) {
|
||||
policies.push_back(std::make_shared<WatchItemPolicy>(
|
||||
[name UTF8String], path_type_pair.first, path_type_pair.second, allow_read_access, audit_only,
|
||||
std::get<ProcessList>(proc_list)));
|
||||
NSStringToUTF8StringView(name), path_type_pair.first, path_type_pair.second,
|
||||
allow_read_access, audit_only, invert_process_exceptions, enable_silent_mode,
|
||||
enable_silent_tty_mode,
|
||||
NSStringToUTF8StringView(options[kWatchItemConfigKeyOptionsCustomMessage]),
|
||||
options[kWatchItemConfigKeyOptionsEventDetailURL],
|
||||
options[kWatchItemConfigKeyOptionsEventDetailText], std::get<ProcessList>(proc_list)));
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -472,6 +545,16 @@ bool ParseConfig(NSDictionary *config, std::vector<std::shared_ptr<WatchItemPoli
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VerifyConfigKey(config, kWatchItemConfigKeyEventDetailURL, [NSString class], err, false,
|
||||
LenRangeValidator(0, kWatchItemConfigEventDetailURLMaxLength))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VerifyConfigKey(config, kWatchItemConfigKeyEventDetailText, [NSString class], err, false,
|
||||
LenRangeValidator(0, kWatchItemConfigEventDetailTextMaxLength))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config[kWatchItemConfigKeyWatchItems] &&
|
||||
![config[kWatchItemConfigKeyWatchItems] isKindOfClass:[NSDictionary class]]) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Top level key '%@' must be a dictionary",
|
||||
@@ -520,24 +603,53 @@ bool ParseConfig(NSDictionary *config, std::vector<std::shared_ptr<WatchItemPoli
|
||||
|
||||
std::shared_ptr<WatchItems> WatchItems::Create(NSString *config_path,
|
||||
uint64_t reapply_config_frequency_secs) {
|
||||
return CreateInternal(config_path, nil, reapply_config_frequency_secs);
|
||||
}
|
||||
|
||||
std::shared_ptr<WatchItems> WatchItems::Create(NSDictionary *config,
|
||||
uint64_t reapply_config_frequency_secs) {
|
||||
return CreateInternal(nil, config, reapply_config_frequency_secs);
|
||||
}
|
||||
|
||||
std::shared_ptr<WatchItems> WatchItems::CreateInternal(NSString *config_path, NSDictionary *config,
|
||||
uint64_t reapply_config_frequency_secs) {
|
||||
if (reapply_config_frequency_secs < kMinReapplyConfigFrequencySecs) {
|
||||
LOGW(@"Invalid watch item update interval provided: %llu. Min allowed: %llu",
|
||||
reapply_config_frequency_secs, kMinReapplyConfigFrequencySecs);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (config_path && config) {
|
||||
LOGW(@"Invalid arguments creating WatchItems - both config and config_path cannot be set.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
dispatch_queue_t q = dispatch_queue_create("com.google.santa.daemon.watch_items.q",
|
||||
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
|
||||
dispatch_source_t timer_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
|
||||
dispatch_source_set_timer(timer_source, dispatch_time(DISPATCH_TIME_NOW, 0),
|
||||
NSEC_PER_SEC * reapply_config_frequency_secs, 0);
|
||||
|
||||
return std::make_shared<WatchItems>(config_path, q, timer_source);
|
||||
if (config_path) {
|
||||
return std::make_shared<WatchItems>(config_path, q, timer_source);
|
||||
} else {
|
||||
return std::make_shared<WatchItems>(config, q, timer_source);
|
||||
}
|
||||
}
|
||||
|
||||
WatchItems::WatchItems(NSString *config_path, dispatch_queue_t q, dispatch_source_t timer_source,
|
||||
void (^periodic_task_complete_f)(void))
|
||||
: config_path_(config_path),
|
||||
embedded_config_(nil),
|
||||
q_(q),
|
||||
timer_source_(timer_source),
|
||||
periodic_task_complete_f_(periodic_task_complete_f),
|
||||
watch_items_(std::make_unique<WatchItemsTree>()) {}
|
||||
|
||||
WatchItems::WatchItems(NSDictionary *config, dispatch_queue_t q, dispatch_source_t timer_source,
|
||||
void (^periodic_task_complete_f)(void))
|
||||
: config_path_(nil),
|
||||
embedded_config_(config),
|
||||
q_(q),
|
||||
timer_source_(timer_source),
|
||||
periodic_task_complete_f_(periodic_task_complete_f),
|
||||
@@ -616,9 +728,19 @@ void WatchItems::UpdateCurrentState(
|
||||
std::swap(currently_monitored_paths_, new_monitored_paths);
|
||||
current_config_ = new_config;
|
||||
if (new_config) {
|
||||
policy_version_ = [new_config[kWatchItemConfigKeyVersion] UTF8String];
|
||||
policy_version_ = NSStringToUTF8String(new_config[kWatchItemConfigKeyVersion]);
|
||||
// Non-existent kWatchItemConfigKeyEventDetailURL key or zero length value
|
||||
// will both result in a nil global policy event detail URL.
|
||||
if (((NSString *)new_config[kWatchItemConfigKeyEventDetailURL]).length) {
|
||||
policy_event_detail_url_ = new_config[kWatchItemConfigKeyEventDetailURL];
|
||||
} else {
|
||||
policy_event_detail_url_ = nil;
|
||||
}
|
||||
policy_event_detail_text_ = new_config[kWatchItemConfigKeyEventDetailText];
|
||||
} else {
|
||||
policy_version_ = "";
|
||||
policy_event_detail_url_ = nil;
|
||||
policy_event_detail_text_ = nil;
|
||||
}
|
||||
|
||||
last_update_time_ = [[NSDate date] timeIntervalSince1970];
|
||||
@@ -688,7 +810,7 @@ void WatchItems::BeginPeriodicTask() {
|
||||
return;
|
||||
}
|
||||
|
||||
shared_watcher->ReloadConfig(shared_watcher->ReadConfig());
|
||||
shared_watcher->ReloadConfig(embedded_config_ ?: shared_watcher->ReadConfig());
|
||||
|
||||
if (shared_watcher->periodic_task_complete_f_) {
|
||||
shared_watcher->periodic_task_complete_f_();
|
||||
@@ -718,11 +840,21 @@ void WatchItems::SetConfigPath(NSString *config_path) {
|
||||
{
|
||||
absl::MutexLock lock(&lock_);
|
||||
config_path_ = config_path;
|
||||
embedded_config_ = nil;
|
||||
config = ReadConfigLocked();
|
||||
}
|
||||
ReloadConfig(config);
|
||||
}
|
||||
|
||||
void WatchItems::SetConfig(NSDictionary *config) {
|
||||
{
|
||||
absl::MutexLock lock(&lock_);
|
||||
config_path_ = nil;
|
||||
embedded_config_ = config;
|
||||
}
|
||||
ReloadConfig(embedded_config_);
|
||||
}
|
||||
|
||||
std::optional<WatchItemsState> WatchItems::State() {
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
|
||||
@@ -740,4 +872,29 @@ std::optional<WatchItemsState> WatchItems::State() {
|
||||
return state;
|
||||
}
|
||||
|
||||
std::pair<NSString *, NSString *> WatchItems::EventDetailLinkInfo(
|
||||
const std::shared_ptr<WatchItemPolicy> &watch_item) {
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
if (!watch_item) {
|
||||
return {policy_event_detail_url_, policy_event_detail_text_};
|
||||
}
|
||||
|
||||
NSString *url = watch_item->event_detail_url.has_value() ? watch_item->event_detail_url.value()
|
||||
: policy_event_detail_url_;
|
||||
|
||||
NSString *text = watch_item->event_detail_text.has_value() ? watch_item->event_detail_text.value()
|
||||
: policy_event_detail_text_;
|
||||
|
||||
// Ensure empty strings are repplaced with nil
|
||||
if (!url.length) {
|
||||
url = nil;
|
||||
}
|
||||
|
||||
if (!text.length) {
|
||||
text = nil;
|
||||
}
|
||||
|
||||
return {url, text};
|
||||
}
|
||||
|
||||
} // namespace santa::santad::data_layer
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
using santa::common::Unit;
|
||||
using santa::santad::data_layer::kWatchItemPolicyDefaultAllowReadAccess;
|
||||
using santa::santad::data_layer::kWatchItemPolicyDefaultAuditOnly;
|
||||
using santa::santad::data_layer::kWatchItemPolicyDefaultInvertProcessExceptions;
|
||||
using santa::santad::data_layer::kWatchItemPolicyDefaultPathType;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
using santa::santad::data_layer::WatchItemPolicy;
|
||||
@@ -61,8 +62,14 @@ extern std::variant<Unit, santatest::ProcessList> VerifyConfigWatchItemProcesses
|
||||
NSDictionary *watch_item, NSError **err);
|
||||
class WatchItemsPeer : public WatchItems {
|
||||
public:
|
||||
using WatchItems::ReloadConfig;
|
||||
using WatchItems::WatchItems;
|
||||
|
||||
using WatchItems::ReloadConfig;
|
||||
using WatchItems::SetConfig;
|
||||
using WatchItems::SetConfigPath;
|
||||
|
||||
using WatchItems::config_path_;
|
||||
using WatchItems::embedded_config_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::data_layer
|
||||
@@ -86,10 +93,6 @@ static NSMutableDictionary *WrapWatchItemsConfig(NSDictionary *config) {
|
||||
return [@{@"Version" : @(kVersion.data()), @"WatchItems" : [config mutableCopy]} mutableCopy];
|
||||
}
|
||||
|
||||
static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
return [@"" stringByPaddingToLength:len withString:str startingAtIndex:0];
|
||||
}
|
||||
|
||||
@interface WatchItemsTest : XCTestCase
|
||||
@property NSFileManager *fileMgr;
|
||||
@property NSString *testDir;
|
||||
@@ -191,7 +194,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
// Changes in config dictionary will update policy info even if the
|
||||
// filesystem didn't change.
|
||||
{
|
||||
WatchItemsPeer watchItems(nil, NULL, NULL);
|
||||
WatchItemsPeer watchItems((NSString *)nil, NULL, NULL);
|
||||
[self pushd:@"a"];
|
||||
watchItems.ReloadConfig(configAllFilesOriginal);
|
||||
|
||||
@@ -212,7 +215,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
|
||||
// Changes to fileystem structure are reflected when a config is reloaded
|
||||
{
|
||||
WatchItemsPeer watchItems(nil, NULL, NULL);
|
||||
WatchItemsPeer watchItems((NSString *)nil, NULL, NULL);
|
||||
[self pushd:@"a"];
|
||||
watchItems.ReloadConfig(configAllFilesOriginal);
|
||||
[self popd];
|
||||
@@ -313,7 +316,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
}
|
||||
});
|
||||
|
||||
WatchItemsPeer watchItems(nil, NULL, NULL);
|
||||
WatchItemsPeer watchItems((NSString *)nil, NULL, NULL);
|
||||
WatchItems::VersionAndPolicies policies;
|
||||
|
||||
// Resultant vector is same size as input vector
|
||||
@@ -750,26 +753,66 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
&err));
|
||||
|
||||
// Options keys must be valid types
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAllowReadAccess : @""}
|
||||
},
|
||||
policies, &err));
|
||||
XCTAssertTrue(ParseConfigSingleWatchItem(@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAllowReadAccess : @(0)}
|
||||
},
|
||||
policies, &err));
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAuditOnly : @""}
|
||||
},
|
||||
policies, &err));
|
||||
XCTAssertTrue(ParseConfigSingleWatchItem(@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAuditOnly : @(0)}
|
||||
},
|
||||
policies, &err));
|
||||
{
|
||||
// Check bool option keys
|
||||
for (NSString *key in @[
|
||||
kWatchItemConfigKeyOptionsAllowReadAccess,
|
||||
kWatchItemConfigKeyOptionsAuditOnly,
|
||||
kWatchItemConfigKeyOptionsInvertProcessExceptions,
|
||||
kWatchItemConfigKeyOptionsEnableSilentMode,
|
||||
kWatchItemConfigKeyOptionsEnableSilentTTYMode,
|
||||
]) {
|
||||
// Parse bool option with invliad type
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(
|
||||
@"",
|
||||
@{kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{key : @""}},
|
||||
policies, &err));
|
||||
|
||||
// Parse bool option with valid type
|
||||
XCTAssertTrue(ParseConfigSingleWatchItem(
|
||||
@"",
|
||||
@{kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{key : @(0)}},
|
||||
policies, &err));
|
||||
}
|
||||
|
||||
// Check other option keys
|
||||
|
||||
// kWatchItemConfigKeyOptionsCustomMessage - Invalid type
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(
|
||||
@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsCustomMessage : @[]}
|
||||
},
|
||||
policies, &err));
|
||||
|
||||
// kWatchItemConfigKeyOptionsCustomMessage zero length
|
||||
XCTAssertTrue(ParseConfigSingleWatchItem(
|
||||
@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsCustomMessage : @""}
|
||||
},
|
||||
policies, &err));
|
||||
|
||||
// kWatchItemConfigKeyOptionsCustomMessage valid "normal" length
|
||||
XCTAssertTrue(ParseConfigSingleWatchItem(
|
||||
@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions :
|
||||
@{kWatchItemConfigKeyOptionsCustomMessage : @"This is a custom message"}
|
||||
},
|
||||
policies, &err));
|
||||
|
||||
// kWatchItemConfigKeyOptionsCustomMessage Invalid "long" length
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(
|
||||
@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions :
|
||||
@{kWatchItemConfigKeyOptionsCustomMessage : RepeatedString(@"A", 4096)}
|
||||
},
|
||||
policies, &err));
|
||||
}
|
||||
|
||||
// If processes are specified, they must be valid format
|
||||
// Note: Full tests in `testVerifyConfigWatchItemProcesses`
|
||||
@@ -784,9 +827,11 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertTrue(
|
||||
ParseConfigSingleWatchItem(@"rule", @{kWatchItemConfigKeyPaths : @[ @"a" ]}, policies, &err));
|
||||
XCTAssertEqual(policies.size(), 1);
|
||||
XCTAssertEqual(*policies[0].get(), WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType,
|
||||
kWatchItemPolicyDefaultAllowReadAccess,
|
||||
kWatchItemPolicyDefaultAuditOnly, {}));
|
||||
XCTAssertEqual(
|
||||
*policies[0].get(),
|
||||
WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType,
|
||||
kWatchItemPolicyDefaultAllowReadAccess, kWatchItemPolicyDefaultAuditOnly,
|
||||
kWatchItemPolicyDefaultInvertProcessExceptions));
|
||||
|
||||
// Test multiple paths, options, and processes
|
||||
policies.clear();
|
||||
@@ -800,7 +845,11 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
@[ @"a", @{kWatchItemConfigKeyPathsPath : @"b", kWatchItemConfigKeyPathsIsPrefix : @(YES)} ],
|
||||
kWatchItemConfigKeyOptions : @{
|
||||
kWatchItemConfigKeyOptionsAllowReadAccess : @(YES),
|
||||
kWatchItemConfigKeyOptionsAuditOnly : @(NO)
|
||||
kWatchItemConfigKeyOptionsAuditOnly : @(NO),
|
||||
kWatchItemConfigKeyOptionsInvertProcessExceptions : @(YES),
|
||||
kWatchItemConfigKeyOptionsEnableSilentMode : @(YES),
|
||||
kWatchItemConfigKeyOptionsEnableSilentMode : @(NO),
|
||||
kWatchItemConfigKeyOptionsCustomMessage : @"",
|
||||
},
|
||||
kWatchItemConfigKeyProcesses : @[
|
||||
@{kWatchItemConfigKeyProcessesBinaryPath : @"pa"},
|
||||
@@ -808,11 +857,14 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
]
|
||||
},
|
||||
policies, &err));
|
||||
|
||||
XCTAssertEqual(policies.size(), 2);
|
||||
XCTAssertEqual(*policies[0].get(),
|
||||
WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType, true, false, procs));
|
||||
WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType, true, false, true,
|
||||
true, false, "", nil, nil, procs));
|
||||
XCTAssertEqual(*policies[1].get(),
|
||||
WatchItemPolicy("rule", "b", WatchItemPathType::kPrefix, true, false, procs));
|
||||
WatchItemPolicy("rule", "b", WatchItemPathType::kPrefix, true, false, true, true,
|
||||
false, "", nil, nil, procs));
|
||||
}
|
||||
|
||||
- (void)testState {
|
||||
@@ -842,4 +894,23 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertGreaterThanOrEqual(state.last_config_load_epoch, startTime);
|
||||
}
|
||||
|
||||
- (void)testSetConfigAndSetConfigPath {
|
||||
// Test internal state when switching back and forth between path-based and
|
||||
// dictionary-based config options.
|
||||
WatchItemsPeer watchItems(@{}, NULL, NULL);
|
||||
|
||||
XCTAssertNil(watchItems.config_path_);
|
||||
XCTAssertNotNil(watchItems.embedded_config_);
|
||||
|
||||
watchItems.SetConfigPath(@"/path/to/a/nonexistent/file/so/nothing/is/opened");
|
||||
|
||||
XCTAssertNotNil(watchItems.config_path_);
|
||||
XCTAssertNil(watchItems.embedded_config_);
|
||||
|
||||
watchItems.SetConfig(@{});
|
||||
|
||||
XCTAssertNil(watchItems.config_path_);
|
||||
XCTAssertNotNil(watchItems.embedded_config_);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#include "Source/common/SantaCache.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
@@ -33,16 +34,31 @@ enum class FlushCacheMode {
|
||||
kAllCaches,
|
||||
};
|
||||
|
||||
enum class FlushCacheReason {
|
||||
kClientModeChanged,
|
||||
kPathRegexChanged,
|
||||
kRulesChanged,
|
||||
kStaticRulesChanged,
|
||||
kExplicitCommand,
|
||||
kFilesystemUnmounted,
|
||||
kEntitlementsPrefixFilterChanged,
|
||||
kEntitlementsTeamIDFilterChanged,
|
||||
};
|
||||
|
||||
class AuthResultCache {
|
||||
public:
|
||||
// Santa currently only flushes caches when new DENY rules are added, not
|
||||
// ALLOW rules. This means this value should be low enough so that if a
|
||||
// ALLOW rules. This means cache_deny_time_ms should be low enough so that if a
|
||||
// previously denied binary is allowed, it can be re-executed by the user in a
|
||||
// timely manner. But the value should be high enough to allow the cache to be
|
||||
// effective in the event the binary is executed in rapid succession.
|
||||
static std::unique_ptr<AuthResultCache> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
SNTMetricSet *metric_set, uint64_t cache_deny_time_ms = 1500);
|
||||
|
||||
AuthResultCache(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms = 1500);
|
||||
SNTMetricCounter *flush_count, uint64_t cache_deny_time_ms = 1500);
|
||||
virtual ~AuthResultCache();
|
||||
|
||||
AuthResultCache(AuthResultCache &&other) = delete;
|
||||
@@ -55,7 +71,7 @@ class AuthResultCache {
|
||||
virtual SNTAction CheckCache(const es_file_t *es_file);
|
||||
virtual SNTAction CheckCache(SantaVnode vnode_id);
|
||||
|
||||
virtual void FlushCache(FlushCacheMode mode);
|
||||
virtual void FlushCache(FlushCacheMode mode, FlushCacheReason reason);
|
||||
|
||||
virtual NSArray<NSNumber *> *CacheCounts();
|
||||
|
||||
@@ -66,6 +82,7 @@ class AuthResultCache {
|
||||
SantaCache<SantaVnode, uint64_t> *nonroot_cache_;
|
||||
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
|
||||
SNTMetricCounter *flush_count_;
|
||||
uint64_t root_devno_;
|
||||
uint64_t cache_deny_time_ns_;
|
||||
dispatch_queue_t q_;
|
||||
|
||||
@@ -25,6 +25,17 @@
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
|
||||
static NSString *const kFlushCacheReasonClientModeChanged = @"ClientModeChanged";
|
||||
static NSString *const kFlushCacheReasonPathRegexChanged = @"PathRegexChanged";
|
||||
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 {
|
||||
|
||||
static inline uint64_t GetCurrentUptime() {
|
||||
@@ -44,9 +55,41 @@ static inline uint64_t TimestampFromCachedValue(uint64_t cachedValue) {
|
||||
return (cachedValue & ~(0xFF00000000000000));
|
||||
}
|
||||
|
||||
NSString *const FlushCacheReasonToString(FlushCacheReason reason) {
|
||||
switch (reason) {
|
||||
case FlushCacheReason::kClientModeChanged: return kFlushCacheReasonClientModeChanged;
|
||||
case FlushCacheReason::kPathRegexChanged: return kFlushCacheReasonPathRegexChanged;
|
||||
case FlushCacheReason::kRulesChanged: return kFlushCacheReasonRulesChanged;
|
||||
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)];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<AuthResultCache> AuthResultCache::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTMetricSet *metric_set,
|
||||
uint64_t cache_deny_time_ms) {
|
||||
SNTMetricCounter *flush_count =
|
||||
[metric_set counterWithName:@"/santa/flush_count"
|
||||
fieldNames:@[ @"Reason" ]
|
||||
helpText:@"Count of times the auth result cache is flushed by reason"];
|
||||
|
||||
return std::make_unique<AuthResultCache>(esapi, flush_count, cache_deny_time_ms);
|
||||
}
|
||||
|
||||
AuthResultCache::AuthResultCache(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms)
|
||||
: esapi_(esapi), cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
|
||||
SNTMetricCounter *flush_count, uint64_t cache_deny_time_ms)
|
||||
: esapi_(esapi),
|
||||
flush_count_(flush_count),
|
||||
cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
|
||||
root_cache_ = new SantaCache<SantaVnode, uint64_t>();
|
||||
nonroot_cache_ = new SantaCache<SantaVnode, uint64_t>();
|
||||
|
||||
@@ -118,7 +161,7 @@ SantaCache<SantaVnode, uint64_t> *AuthResultCache::CacheForVnodeID(SantaVnode vn
|
||||
return (vnode_id.fsid == root_devno_ || root_devno_ == 0) ? root_cache_ : nonroot_cache_;
|
||||
}
|
||||
|
||||
void AuthResultCache::FlushCache(FlushCacheMode mode) {
|
||||
void AuthResultCache::FlushCache(FlushCacheMode mode, FlushCacheReason reason) {
|
||||
nonroot_cache_->clear();
|
||||
if (mode == FlushCacheMode::kAllCaches) {
|
||||
root_cache_->clear();
|
||||
@@ -134,6 +177,8 @@ void AuthResultCache::FlushCache(FlushCacheMode mode) {
|
||||
shared_esapi->ClearCache(Client());
|
||||
});
|
||||
}
|
||||
|
||||
[flush_count_ incrementForFieldValues:@[ FlushCacheReasonToString(reason) ]];
|
||||
}
|
||||
|
||||
NSArray<NSNumber *> *AuthResultCache::CacheCounts() {
|
||||
|
||||
@@ -29,6 +29,13 @@
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::FlushCacheReason;
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
extern NSString *const FlushCacheReasonToString(FlushCacheReason reason);
|
||||
}
|
||||
|
||||
using santa::santad::event_providers::FlushCacheReasonToString;
|
||||
|
||||
// Grab the st_dev number of the root volume to match the root cache
|
||||
static uint64_t RootDevno() {
|
||||
@@ -66,14 +73,14 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
|
||||
- (void)testEmptyCacheExpectedNumberOfCacheCounts {
|
||||
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(esapi);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(esapi, nil);
|
||||
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
- (void)testBasicOperation {
|
||||
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(esapi);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(esapi, nil);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 222);
|
||||
@@ -110,7 +117,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
|
||||
- (void)testFlushCache {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(mockESApi, nil);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 111);
|
||||
@@ -121,7 +128,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
|
||||
// Flush non-root only
|
||||
cache->FlushCache(FlushCacheMode::kNonRootOnly);
|
||||
cache->FlushCache(FlushCacheMode::kNonRootOnly, FlushCacheReason::kClientModeChanged);
|
||||
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
|
||||
@@ -138,7 +145,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
dispatch_semaphore_signal(sema);
|
||||
return true;
|
||||
}));
|
||||
cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
cache->FlushCache(FlushCacheMode::kAllCaches, FlushCacheReason::kClientModeChanged);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
|
||||
@@ -151,7 +158,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
|
||||
- (void)testCacheStateMachine {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(mockESApi, nil);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
@@ -193,7 +200,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
// Create a cache with a lowered cache expiry value
|
||||
uint64_t expiryMS = 250;
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi, expiryMS);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(mockESApi, nil, expiryMS);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
@@ -215,4 +222,24 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
- (void)testFlushCacheReasonToString {
|
||||
std::map<FlushCacheReason, NSString *> reasonToString = {
|
||||
{FlushCacheReason::kClientModeChanged, @"ClientModeChanged"},
|
||||
{FlushCacheReason::kPathRegexChanged, @"PathRegexChanged"},
|
||||
{FlushCacheReason::kRulesChanged, @"RulesChanged"},
|
||||
{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)(static_cast<int>(FlushCacheReason::kEntitlementsTeamIDFilterChanged) + 1)));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <DiskArbitration/DiskArbitration.h>
|
||||
#include <Foundation/Foundation.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/ucred.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -27,6 +30,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@interface MockDADisk : NSObject
|
||||
@property(nonatomic) NSDictionary *diskDescription;
|
||||
@property(nonatomic, readwrite) NSString *name;
|
||||
@property(nonatomic) BOOL wasMounted;
|
||||
@property(nonatomic) BOOL wasUnmounted;
|
||||
@end
|
||||
|
||||
typedef void (^MockDADiskAppearedCallback)(DADiskRef ref);
|
||||
@@ -36,19 +41,35 @@ typedef void (^MockDADiskAppearedCallback)(DADiskRef ref);
|
||||
NSMutableDictionary<NSString *, MockDADisk *> *insertedDevices;
|
||||
@property(nonatomic, readwrite, nonnull)
|
||||
NSMutableArray<MockDADiskAppearedCallback> *diskAppearedCallbacks;
|
||||
@property(nonatomic) BOOL wasRemounted;
|
||||
@property(nonatomic, nullable) dispatch_queue_t sessionQueue;
|
||||
|
||||
- (instancetype _Nonnull)init;
|
||||
- (void)reset;
|
||||
|
||||
// Also triggers DADiskRegisterDiskAppearedCallback
|
||||
- (void)insert:(MockDADisk *)ref bsdName:(NSString *)bsdName;
|
||||
- (void)insert:(MockDADisk *)ref;
|
||||
|
||||
// Retrieve an initialized singleton MockDiskArbitration object
|
||||
+ (instancetype _Nonnull)mockDiskArbitration;
|
||||
@end
|
||||
|
||||
@interface MockStatfs : NSObject
|
||||
@property NSString *fromName;
|
||||
@property NSString *onName;
|
||||
@property NSNumber *flags;
|
||||
|
||||
- (instancetype _Nonnull)initFrom:(NSString *)from on:(NSString *)on flags:(NSNumber *)flags;
|
||||
@end
|
||||
|
||||
@interface MockMounts : NSObject
|
||||
@property(nonatomic) NSMutableDictionary<NSString *, MockStatfs *> *mounts;
|
||||
|
||||
- (instancetype _Nonnull)init;
|
||||
- (void)reset;
|
||||
- (void)insert:(MockStatfs *)sfs;
|
||||
+ (instancetype _Nonnull)mockMounts;
|
||||
@end
|
||||
|
||||
//
|
||||
// All DiskArbitration functions used in SNTEndpointSecurityDeviceManager
|
||||
// and shimmed out accordingly.
|
||||
@@ -81,5 +102,9 @@ void DARegisterDiskDescriptionChangedCallback(DASessionRef session,
|
||||
void DASessionSetDispatchQueue(DASessionRef session, dispatch_queue_t __nullable queue);
|
||||
DASessionRef __nullable DASessionCreate(CFAllocatorRef __nullable allocator);
|
||||
|
||||
void DADiskUnmount(DADiskRef disk, DADiskUnmountOptions options,
|
||||
DADiskUnmountCallback __nullable callback, void *__nullable context);
|
||||
int getmntinfo_r_np(struct statfs *__nullable *__nullable mntbufp, int flags);
|
||||
|
||||
CF_EXTERN_C_END
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/ucred.h>
|
||||
|
||||
#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h"
|
||||
|
||||
@@ -37,11 +40,14 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
[self.insertedDevices removeAllObjects];
|
||||
[self.diskAppearedCallbacks removeAllObjects];
|
||||
self.sessionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
self.wasRemounted = NO;
|
||||
}
|
||||
|
||||
- (void)insert:(MockDADisk *)ref bsdName:(NSString *)bsdName {
|
||||
self.insertedDevices[bsdName] = ref;
|
||||
- (void)insert:(MockDADisk *)ref {
|
||||
if (!ref.diskDescription[@"DAMediaBSDName"]) {
|
||||
[NSException raise:@"Missing DAMediaBSDName"
|
||||
format:@"The MockDADisk is missing the DAMediaBSDName diskDescription key."];
|
||||
}
|
||||
self.insertedDevices[ref.diskDescription[@"DAMediaBSDName"]] = ref;
|
||||
|
||||
for (MockDADiskAppearedCallback callback in self.diskAppearedCallbacks) {
|
||||
dispatch_sync(self.sessionQueue, ^{
|
||||
@@ -62,12 +68,58 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@end
|
||||
|
||||
@implementation MockStatfs
|
||||
- (instancetype _Nonnull)initFrom:(NSString *)from on:(NSString *)on flags:(NSNumber *)flags {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_fromName = from;
|
||||
_onName = on;
|
||||
_flags = flags;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation MockMounts
|
||||
|
||||
- (instancetype _Nonnull)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_mounts = [NSMutableDictionary dictionary];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)reset {
|
||||
[self.mounts removeAllObjects];
|
||||
}
|
||||
|
||||
- (void)insert:(MockStatfs *)sfs {
|
||||
self.mounts[sfs.fromName] = sfs;
|
||||
}
|
||||
|
||||
+ (instancetype _Nonnull)mockMounts {
|
||||
static MockMounts *sharedMounts;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedMounts = [[MockMounts alloc] init];
|
||||
});
|
||||
return sharedMounts;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
void DADiskMountWithArguments(DADiskRef _Nonnull disk, CFURLRef __nullable path,
|
||||
DADiskMountOptions options, DADiskMountCallback __nullable callback,
|
||||
void *__nullable context,
|
||||
CFStringRef __nullable arguments[_Nullable]) {
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
mockDA.wasRemounted = YES;
|
||||
MockDADisk *mockDisk = (__bridge MockDADisk *)disk;
|
||||
mockDisk.wasMounted = YES;
|
||||
|
||||
if (context) {
|
||||
dispatch_semaphore_t sema = (__bridge dispatch_semaphore_t)context;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}
|
||||
}
|
||||
|
||||
DADiskRef __nullable DADiskCreateFromBSDName(CFAllocatorRef __nullable allocator,
|
||||
@@ -117,4 +169,32 @@ DASessionRef __nullable DASessionCreate(CFAllocatorRef __nullable allocator) {
|
||||
return (__bridge DASessionRef)[MockDiskArbitration mockDiskArbitration];
|
||||
};
|
||||
|
||||
void DADiskUnmount(DADiskRef disk, DADiskUnmountOptions options,
|
||||
DADiskUnmountCallback __nullable callback, void *__nullable context) {
|
||||
MockDADisk *mockDisk = (__bridge MockDADisk *)disk;
|
||||
mockDisk.wasUnmounted = YES;
|
||||
|
||||
dispatch_semaphore_t sema = (__bridge dispatch_semaphore_t)context;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}
|
||||
|
||||
int getmntinfo_r_np(struct statfs *__nullable *__nullable mntbufp, int flags) {
|
||||
MockMounts *mockMounts = [MockMounts mockMounts];
|
||||
|
||||
struct statfs *sfs = (struct statfs *)calloc(mockMounts.mounts.count, sizeof(struct statfs));
|
||||
|
||||
__block NSUInteger i = 0;
|
||||
[mockMounts.mounts
|
||||
enumerateKeysAndObjectsUsingBlock:^(NSString *key, MockStatfs *mockSfs, BOOL *stop) {
|
||||
strlcpy(sfs[i].f_mntfromname, mockSfs.fromName.UTF8String, sizeof(sfs[i].f_mntfromname));
|
||||
strlcpy(sfs[i].f_mntonname, mockSfs.onName.UTF8String, sizeof(sfs[i].f_mntonname));
|
||||
sfs[i].f_flags = [mockSfs.flags unsignedIntValue];
|
||||
i++;
|
||||
}];
|
||||
|
||||
*mntbufp = sfs;
|
||||
|
||||
return (int)mockMounts.mounts.count;
|
||||
}
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
|
||||
@@ -46,7 +46,11 @@ class EnrichedFile {
|
||||
group_(std::move(other.group_)),
|
||||
hash_(std::move(other.hash_)) {}
|
||||
|
||||
// Note: Move assignment could be safely implemented but not currently needed
|
||||
EnrichedFile &operator=(EnrichedFile &&other) = delete;
|
||||
|
||||
EnrichedFile(const EnrichedFile &other) = delete;
|
||||
EnrichedFile &operator=(const EnrichedFile &other) = delete;
|
||||
|
||||
const std::optional<std::shared_ptr<std::string>> &user() const {
|
||||
return user_;
|
||||
@@ -87,7 +91,11 @@ class EnrichedProcess {
|
||||
real_group_(std::move(other.real_group_)),
|
||||
executable_(std::move(other.executable_)) {}
|
||||
|
||||
// Note: Move assignment could be safely implemented but not currently needed
|
||||
EnrichedProcess &operator=(EnrichedProcess &&other) = delete;
|
||||
|
||||
EnrichedProcess(const EnrichedProcess &other) = delete;
|
||||
EnrichedProcess &operator=(const EnrichedProcess &other) = delete;
|
||||
|
||||
const std::optional<std::shared_ptr<std::string>> &effective_user() const {
|
||||
return effective_user_;
|
||||
@@ -123,7 +131,12 @@ class EnrichedEventType {
|
||||
instigator_(std::move(other.instigator_)),
|
||||
enrichment_time_(std::move(other.enrichment_time_)) {}
|
||||
|
||||
// Note: Move assignment could be safely implemented but not currently needed
|
||||
// so no sense in implementing across all child classes
|
||||
EnrichedEventType &operator=(EnrichedEventType &&other) = delete;
|
||||
|
||||
EnrichedEventType(const EnrichedEventType &other) = delete;
|
||||
EnrichedEventType &operator=(const EnrichedEventType &other) = delete;
|
||||
|
||||
virtual ~EnrichedEventType() = default;
|
||||
|
||||
@@ -307,9 +320,19 @@ class EnrichedUnlink : public EnrichedEventType {
|
||||
EnrichedFile target_;
|
||||
};
|
||||
|
||||
class EnrichedCSInvalidated : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedCSInvalidated(Message &&es_msg, EnrichedProcess &&instigator)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)) {}
|
||||
EnrichedCSInvalidated(EnrichedCSInvalidated &&other)
|
||||
: EnrichedEventType(std::move(other)) {}
|
||||
EnrichedCSInvalidated(const EnrichedCSInvalidated &other) = delete;
|
||||
};
|
||||
|
||||
using EnrichedType =
|
||||
std::variant<EnrichedClose, EnrichedExchange, EnrichedExec, EnrichedExit,
|
||||
EnrichedFork, EnrichedLink, EnrichedRename, EnrichedUnlink>;
|
||||
EnrichedFork, EnrichedLink, EnrichedRename, EnrichedUnlink,
|
||||
EnrichedCSInvalidated>;
|
||||
|
||||
class EnrichedMessage {
|
||||
public:
|
||||
|
||||
@@ -34,7 +34,7 @@ class Enricher {
|
||||
public:
|
||||
Enricher();
|
||||
virtual ~Enricher() = default;
|
||||
virtual std::shared_ptr<EnrichedMessage> Enrich(Message &&msg);
|
||||
virtual std::unique_ptr<EnrichedMessage> Enrich(Message &&msg);
|
||||
virtual EnrichedProcess Enrich(
|
||||
const es_process_t &es_proc,
|
||||
EnrichOptions options = EnrichOptions::kDefault);
|
||||
|
||||
@@ -30,19 +30,19 @@ namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Enricher::Enricher() : username_cache_(256), groupname_cache_(256) {}
|
||||
|
||||
std::shared_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
|
||||
std::unique_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
|
||||
// TODO(mlw): Consider potential design patterns that could help reduce memory usage under load
|
||||
// (such as maybe the flyweight pattern)
|
||||
switch (es_msg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedClose(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedClose(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.close.target)));
|
||||
case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedExchange(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedExchange(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.exchangedata.file1),
|
||||
Enrich(*es_msg->event.exchangedata.file2)));
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedExec(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedExec(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.exec.target),
|
||||
(es_msg->version >= 2 && es_msg->event.exec.script)
|
||||
? std::make_optional(Enrich(*es_msg->event.exec.script))
|
||||
@@ -51,29 +51,32 @@ std::shared_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
|
||||
? std::make_optional(Enrich(*es_msg->event.exec.cwd))
|
||||
: std::nullopt));
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedFork(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedFork(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.fork.child)));
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT:
|
||||
return std::make_shared<EnrichedMessage>(
|
||||
return std::make_unique<EnrichedMessage>(
|
||||
EnrichedExit(std::move(es_msg), Enrich(*es_msg->process)));
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK:
|
||||
return std::make_shared<EnrichedMessage>(
|
||||
return std::make_unique<EnrichedMessage>(
|
||||
EnrichedLink(std::move(es_msg), Enrich(*es_msg->process),
|
||||
Enrich(*es_msg->event.link.source), Enrich(*es_msg->event.link.target_dir)));
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: {
|
||||
if (es_msg->event.rename.destination_type == ES_DESTINATION_TYPE_NEW_PATH) {
|
||||
return std::make_shared<EnrichedMessage>(EnrichedRename(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedRename(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.rename.source),
|
||||
std::nullopt, Enrich(*es_msg->event.rename.destination.new_path.dir)));
|
||||
} else {
|
||||
return std::make_shared<EnrichedMessage>(EnrichedRename(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedRename(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.rename.source),
|
||||
Enrich(*es_msg->event.rename.destination.existing_file), std::nullopt));
|
||||
}
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedUnlink(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedUnlink(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.unlink.target)));
|
||||
case ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED:
|
||||
return std::make_unique<EnrichedMessage>(
|
||||
EnrichedCSInvalidated(std::move(es_msg), Enrich(*es_msg->process)));
|
||||
default:
|
||||
// This is a programming error
|
||||
LOGE(@"Attempting to enrich an unhandled event type: %d", es_msg->event_type);
|
||||
|
||||
77
Source/santad/EventProviders/RateLimiter.h
Normal file
77
Source/santad/EventProviders/RateLimiter.h
Normal file
@@ -0,0 +1,77 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_RATELIMITER_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_RATELIMITER_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
// Forward declarations
|
||||
namespace santa::santad::event_providers {
|
||||
class RateLimiterPeer;
|
||||
}
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
// Very basic rate limiting infrastructure.
|
||||
// Currently only handles X events per duration.
|
||||
//
|
||||
// TODO(mlw): Support changing QPS via config
|
||||
// TODO(mlw): Support per-rule QPS
|
||||
// TODO(mlw): Consider adding sliding window support
|
||||
class RateLimiter {
|
||||
public:
|
||||
// Factory
|
||||
static std::shared_ptr<RateLimiter> Create(
|
||||
std::shared_ptr<santa::santad::Metrics> metrics,
|
||||
santa::santad::Processor processor, uint16_t max_qps,
|
||||
NSTimeInterval reset_duration = kDefaultResetDuration);
|
||||
|
||||
RateLimiter(std::shared_ptr<santa::santad::Metrics> metrics,
|
||||
santa::santad::Processor processor, uint16_t max_qps,
|
||||
NSTimeInterval reset_duration);
|
||||
|
||||
enum class Decision {
|
||||
kRateLimited = 0,
|
||||
kAllowed,
|
||||
};
|
||||
|
||||
Decision Decide(uint64_t cur_mach_time);
|
||||
|
||||
friend class santa::santad::event_providers::RateLimiterPeer;
|
||||
|
||||
private:
|
||||
bool ShouldRateLimitLocked();
|
||||
size_t EventsRateLimitedLocked();
|
||||
void TryResetLocked(uint64_t cur_mach_time);
|
||||
|
||||
static constexpr NSTimeInterval kDefaultResetDuration = 15.0;
|
||||
|
||||
std::shared_ptr<santa::santad::Metrics> metrics_;
|
||||
santa::santad::Processor processor_;
|
||||
size_t log_count_total_ = 0;
|
||||
size_t max_log_count_total_;
|
||||
uint64_t reset_mach_time_;
|
||||
uint64_t reset_duration_ns_;
|
||||
dispatch_queue_t q_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers
|
||||
|
||||
#endif
|
||||
85
Source/santad/EventProviders/RateLimiter.mm
Normal file
85
Source/santad/EventProviders/RateLimiter.mm
Normal file
@@ -0,0 +1,85 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/EventProviders/RateLimiter.h"
|
||||
|
||||
#include "Source/common/BranchPrediction.h"
|
||||
#include "Source/common/SystemResources.h"
|
||||
|
||||
using santa::santad::Metrics;
|
||||
using santa::santad::Processor;
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
std::shared_ptr<RateLimiter> RateLimiter::Create(std::shared_ptr<Metrics> metrics,
|
||||
Processor processor, uint16_t max_qps,
|
||||
NSTimeInterval reset_duration) {
|
||||
return std::make_shared<RateLimiter>(std::move(metrics), processor, max_qps, reset_duration);
|
||||
}
|
||||
|
||||
RateLimiter::RateLimiter(std::shared_ptr<Metrics> metrics, Processor processor, uint16_t max_qps,
|
||||
NSTimeInterval reset_duration)
|
||||
: metrics_(std::move(metrics)),
|
||||
processor_(processor),
|
||||
max_log_count_total_(reset_duration * max_qps),
|
||||
reset_mach_time_(0),
|
||||
reset_duration_ns_(reset_duration * NSEC_PER_SEC) {
|
||||
q_ = dispatch_queue_create(
|
||||
"com.google.santa.daemon.rate_limiter",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
|
||||
QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
}
|
||||
|
||||
bool RateLimiter::ShouldRateLimitLocked() {
|
||||
return log_count_total_ > max_log_count_total_;
|
||||
}
|
||||
|
||||
size_t RateLimiter::EventsRateLimitedLocked() {
|
||||
if (unlikely(ShouldRateLimitLocked())) {
|
||||
return log_count_total_ - max_log_count_total_;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void RateLimiter::TryResetLocked(uint64_t cur_mach_time) {
|
||||
if (cur_mach_time > reset_mach_time_) {
|
||||
if (metrics_) {
|
||||
metrics_->SetRateLimitingMetrics(processor_, EventsRateLimitedLocked());
|
||||
}
|
||||
|
||||
log_count_total_ = 0;
|
||||
reset_mach_time_ = AddNanosecondsToMachTime(reset_duration_ns_, cur_mach_time);
|
||||
}
|
||||
}
|
||||
|
||||
RateLimiter::Decision RateLimiter::Decide(uint64_t cur_mach_time) {
|
||||
__block RateLimiter::Decision decision;
|
||||
|
||||
dispatch_sync(q_, ^{
|
||||
TryResetLocked(cur_mach_time);
|
||||
|
||||
++log_count_total_;
|
||||
|
||||
if (unlikely(ShouldRateLimitLocked())) {
|
||||
decision = Decision::kRateLimited;
|
||||
} else {
|
||||
decision = RateLimiter::Decision::kAllowed;
|
||||
}
|
||||
});
|
||||
|
||||
return decision;
|
||||
}
|
||||
|
||||
} // namespace santa::santad::event_providers
|
||||
149
Source/santad/EventProviders/RateLimiterTest.mm
Normal file
149
Source/santad/EventProviders/RateLimiterTest.mm
Normal file
@@ -0,0 +1,149 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/EventProviders/RateLimiter.h"
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#include "Source/common/SystemResources.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
using santa::santad::event_providers::RateLimiter;
|
||||
|
||||
static const santa::santad::Processor kDefaultProcessor =
|
||||
santa::santad::Processor::kFileAccessAuthorizer;
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
class RateLimiterPeer : public RateLimiter {
|
||||
public:
|
||||
using RateLimiter::RateLimiter;
|
||||
|
||||
using RateLimiter::EventsRateLimitedLocked;
|
||||
using RateLimiter::ShouldRateLimitLocked;
|
||||
using RateLimiter::TryResetLocked;
|
||||
|
||||
using RateLimiter::log_count_total_;
|
||||
using RateLimiter::reset_mach_time_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers
|
||||
|
||||
using santa::santad::event_providers::RateLimiterPeer;
|
||||
|
||||
@interface RateLimiterTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation RateLimiterTest
|
||||
|
||||
- (void)testTryResetLocked {
|
||||
// Create an object supporting 1 QPS, and a reset duration of 2s
|
||||
uint16_t maxQps = 1;
|
||||
NSTimeInterval resetDuration = 2;
|
||||
RateLimiterPeer rlp(nullptr, kDefaultProcessor, maxQps, resetDuration);
|
||||
|
||||
// Check the current reset_mach_time_ is 0 so that it gets
|
||||
// set when the first decision is made
|
||||
XCTAssertEqual(rlp.reset_mach_time_, 0);
|
||||
|
||||
// Define our current mach time and create the expected new reset duration floor
|
||||
uint64_t curMachTime = 1;
|
||||
uint64_t expectedMachTime = AddNanosecondsToMachTime(resetDuration, curMachTime);
|
||||
|
||||
// Set a higher log count to ensure it is reset
|
||||
rlp.log_count_total_ = 123;
|
||||
|
||||
rlp.TryResetLocked(curMachTime);
|
||||
|
||||
// Ensure values are reset appropriately
|
||||
XCTAssertEqual(rlp.log_count_total_, 0);
|
||||
XCTAssertGreaterThanOrEqual(rlp.reset_mach_time_, expectedMachTime);
|
||||
|
||||
// Setup values so that calling TryResetLocked shouldn't reset anything
|
||||
size_t expectedLogCount = 123;
|
||||
expectedMachTime = 456;
|
||||
rlp.log_count_total_ = expectedLogCount;
|
||||
rlp.reset_mach_time_ = expectedMachTime;
|
||||
curMachTime = rlp.reset_mach_time_;
|
||||
|
||||
rlp.TryResetLocked(curMachTime);
|
||||
|
||||
// Ensure the values were not changed
|
||||
XCTAssertEqual(rlp.log_count_total_, expectedLogCount);
|
||||
XCTAssertGreaterThanOrEqual(rlp.reset_mach_time_, expectedMachTime);
|
||||
}
|
||||
|
||||
- (void)testDecide {
|
||||
// Create an object supporting 2 QPS, and a reset duration of 4s
|
||||
uint16_t maxQps = 2;
|
||||
NSTimeInterval resetDuration = 4;
|
||||
uint64_t allowedLogsPerDuration = maxQps * resetDuration;
|
||||
RateLimiterPeer rlp(nullptr, kDefaultProcessor, maxQps, resetDuration);
|
||||
|
||||
// Check the current log count is initially 0
|
||||
XCTAssertEqual(rlp.log_count_total_, 0);
|
||||
|
||||
// Make the first decision
|
||||
RateLimiter::Decision gotDecision;
|
||||
|
||||
for (uint64_t i = 0; i < (allowedLogsPerDuration); i++) {
|
||||
gotDecision = rlp.Decide(0);
|
||||
XCTAssertEqual(gotDecision, RateLimiter::Decision::kAllowed);
|
||||
}
|
||||
|
||||
// Ensure the log count is the expected amount
|
||||
XCTAssertEqual(rlp.log_count_total_, allowedLogsPerDuration);
|
||||
|
||||
// Make another decision and ensure the log count still increases and
|
||||
// the decision is rate limited
|
||||
gotDecision = rlp.Decide(0);
|
||||
XCTAssertEqual(gotDecision, RateLimiter::Decision::kRateLimited);
|
||||
XCTAssertEqual(rlp.log_count_total_, allowedLogsPerDuration + 1);
|
||||
|
||||
// Make another decision, though now with the cur mach time greater than
|
||||
// the reset mach time. Then ensure values were appropriately reset.
|
||||
uint64_t oldResetMachTime = rlp.reset_mach_time_;
|
||||
gotDecision = rlp.Decide(rlp.reset_mach_time_ + 1);
|
||||
XCTAssertEqual(gotDecision, RateLimiter::Decision::kAllowed);
|
||||
XCTAssertEqual(rlp.log_count_total_, 1);
|
||||
XCTAssertGreaterThan(rlp.reset_mach_time_, oldResetMachTime);
|
||||
}
|
||||
|
||||
- (void)testShouldRateLimitAndCounts {
|
||||
// Create an object supporting 2 QPS, and a reset duration of 4s
|
||||
uint16_t maxQps = 2;
|
||||
NSTimeInterval resetDuration = 4;
|
||||
uint64_t allowedLogsPerDuration = maxQps * resetDuration;
|
||||
uint64_t logsOverQPS = 5;
|
||||
RateLimiterPeer rlp(nullptr, kDefaultProcessor, maxQps, resetDuration);
|
||||
|
||||
// Initially no rate limiting should apply
|
||||
XCTAssertFalse(rlp.ShouldRateLimitLocked());
|
||||
XCTAssertEqual(rlp.EventsRateLimitedLocked(), 0);
|
||||
|
||||
// Simulate a smmaller volume of logs received than QPS
|
||||
rlp.log_count_total_ = allowedLogsPerDuration - 1;
|
||||
|
||||
XCTAssertFalse(rlp.ShouldRateLimitLocked());
|
||||
XCTAssertEqual(rlp.EventsRateLimitedLocked(), 0);
|
||||
|
||||
// Simulate a larger volume of logs received than QPS
|
||||
rlp.log_count_total_ = allowedLogsPerDuration + logsOverQPS;
|
||||
|
||||
XCTAssertTrue(rlp.ShouldRateLimitLocked());
|
||||
XCTAssertEqual(rlp.EventsRateLimitedLocked(), logsOverQPS);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -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"
|
||||
@@ -85,6 +85,15 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
}
|
||||
|
||||
- (void)testHandleMessage {
|
||||
#ifdef THREAD_SANITIZER
|
||||
// TSAN and this test do not get along in multiple ways.
|
||||
// We get data race false positives in OCMock, and timeouts
|
||||
// waiting for messages processing (presumably due to tsan's scheduling).
|
||||
// Just skip it.
|
||||
XCTSkip(@"TSAN enabled");
|
||||
return;
|
||||
#endif
|
||||
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth);
|
||||
@@ -137,27 +146,31 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
Message msg(mockESApi, &esMsg);
|
||||
// Scope so msg is destructed (and calls ReleaseMessage) before stopMocking is called.
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(NO);
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(NO);
|
||||
|
||||
OCMExpect([mockAuthClient postAction:SNTActionRespondDeny
|
||||
OCMExpect([mockAuthClient postAction:SNTActionRespondDeny
|
||||
forMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs();
|
||||
OCMStub([mockAuthClient postAction:SNTActionRespondDeny
|
||||
forMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs();
|
||||
OCMStub([mockAuthClient postAction:SNTActionRespondDeny forMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(nil);
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(nil);
|
||||
|
||||
[mockAuthClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
[mockAuthClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
}
|
||||
|
||||
[mockAuthClient stopMocking];
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
@@ -180,25 +193,27 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
Message msg(mockESApi, &esMsg);
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(YES);
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(YES);
|
||||
|
||||
OCMExpect([mockAuthClient processMessage:Message(mockESApi, &esMsg)]).ignoringNonObjectArgs();
|
||||
OCMStub([mockAuthClient processMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(nil);
|
||||
OCMExpect([mockAuthClient processMessage:Message(mockESApi, &esMsg)]).ignoringNonObjectArgs();
|
||||
OCMStub([mockAuthClient processMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(nil);
|
||||
|
||||
[mockAuthClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kProcessed);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
[mockAuthClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kProcessed);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
}
|
||||
|
||||
[mockAuthClient stopMocking];
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
@@ -219,7 +234,7 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
EXPECT_CALL(*mockAuthCache, CheckCache)
|
||||
.WillOnce(testing::Return(SNTActionRequestBinary))
|
||||
.WillOnce(testing::Return(SNTActionRequestBinary))
|
||||
@@ -286,7 +301,7 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, SNTActionRespondAllowCompiler))
|
||||
.WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, SNTActionRespondAllow))
|
||||
|
||||
@@ -125,6 +125,11 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
|
||||
self->_esClient = self->_esApi->NewClient(^(es_client_t *c, Message esMsg) {
|
||||
int64_t processingStart = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
|
||||
// Update event stats BEFORE calling into the processor class to ensure
|
||||
// sequence numbers are processed in order.
|
||||
self->_metrics->UpdateEventStats(self->_processor, esMsg.operator->());
|
||||
|
||||
es_event_type_t eventType = esMsg->event_type;
|
||||
if ([self shouldHandleMessage:esMsg]) {
|
||||
[self handleMessage:std::move(esMsg)
|
||||
@@ -193,13 +198,12 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
return _esApi->UnsubscribeAll(_esClient);
|
||||
}
|
||||
|
||||
- (bool)unmuteEverything {
|
||||
bool result = _esApi->UnmuteAllPaths(_esClient);
|
||||
result = _esApi->UnmuteAllTargetPaths(_esClient) && result;
|
||||
return result;
|
||||
- (bool)unmuteAllTargetPaths {
|
||||
return _esApi->UnmuteAllTargetPaths(_esClient);
|
||||
}
|
||||
|
||||
- (bool)enableTargetPathWatching {
|
||||
[self unmuteAllTargetPaths];
|
||||
return _esApi->InvertTargetPathMuting(_esClient);
|
||||
}
|
||||
|
||||
@@ -236,9 +240,9 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
}
|
||||
}
|
||||
|
||||
- (void)processEnrichedMessage:(std::shared_ptr<EnrichedMessage>)msg
|
||||
handler:(void (^)(std::shared_ptr<EnrichedMessage>))messageHandler {
|
||||
__block std::shared_ptr<EnrichedMessage> msgTmp = std::move(msg);
|
||||
- (void)processEnrichedMessage:(std::unique_ptr<EnrichedMessage>)msg
|
||||
handler:(void (^)(std::unique_ptr<EnrichedMessage>))messageHandler {
|
||||
__block std::unique_ptr<EnrichedMessage> msgTmp = std::move(msg);
|
||||
dispatch_async(_notifyQueue, ^{
|
||||
messageHandler(std::move(msgTmp));
|
||||
});
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
- (bool)subscribeAndClearCache:(const std::set<es_event_type_t> &)events;
|
||||
|
||||
- (bool)unsubscribeAll;
|
||||
- (bool)unmuteEverything;
|
||||
- (bool)unmuteAllTargetPaths;
|
||||
- (bool)enableTargetPathWatching;
|
||||
- (bool)muteTargetPaths:
|
||||
(const std::vector<std::pair<std::string, santa::santad::data_layer::WatchItemPathType>> &)paths;
|
||||
@@ -72,9 +72,9 @@
|
||||
|
||||
- (void)
|
||||
processEnrichedMessage:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage>)msg
|
||||
(std::unique_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage>)msg
|
||||
handler:
|
||||
(void (^)(std::shared_ptr<
|
||||
(void (^)(std::unique_ptr<
|
||||
santa::santad::event_providers::endpoint_security::EnrichedMessage>))
|
||||
messageHandler;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user