mirror of
https://github.com/google/santa.git
synced 2026-01-15 01:08:12 -05:00
Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
5
.bazelrc
5
.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
|
||||
|
||||
@@ -1 +1 @@
|
||||
5.3.0
|
||||
6.3.2
|
||||
|
||||
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@v3
|
||||
- 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@v3
|
||||
- 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@v3
|
||||
- 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@v3
|
||||
- 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
|
||||
|
||||
17
.github/workflows/e2e.yml
vendored
17
.github/workflows/e2e.yml
vendored
@@ -1,25 +1,28 @@
|
||||
name: E2E
|
||||
|
||||
on: workflow_dispatch
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 4 * * *' # Every day at 4:00 UTC (not to interfere with fuzzing)
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
start_vm:
|
||||
runs-on: e2e-host
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Start VM
|
||||
run: python3 Testing/integration/actions/start_vm.py macOS_12.bundle.tar.gz
|
||||
run: python3 Testing/integration/actions/start_vm.py macOS_14.bundle.tar.gz
|
||||
|
||||
integration:
|
||||
runs-on: e2e-vm
|
||||
env:
|
||||
VM_PASSWORD: ${{ secrets.VM_PASSWORD }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install configuration profile
|
||||
run: bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Add homebrew to PATH
|
||||
run: echo "/opt/homebrew/bin/" >> $GITHUB_PATH
|
||||
- name: Install configuration profile
|
||||
run: bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig
|
||||
- name: Build, install, and start moroz
|
||||
run: |
|
||||
bazel build @com_github_groob_moroz//cmd/moroz:moroz
|
||||
@@ -36,6 +39,8 @@ jobs:
|
||||
run: ./Testing/integration/test_config_changes.sh
|
||||
- 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
|
||||
|
||||
@@ -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"
|
||||
|
||||
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.
|
||||
|
||||
10
README.md
10
README.md
@@ -7,10 +7,10 @@
|
||||
[](https://github.com/google/santa/releases/latest)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/google/santa/main/Source/gui/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
|
||||
<img src="./docs/images/santa-sleigh-256.png" height="128" alt="Santa Icon" />
|
||||
</p>
|
||||
|
||||
Santa is a binary authorization system for macOS. It consists of a system
|
||||
Santa is a binary and file access authorization system for macOS. It consists of a system
|
||||
extension that monitors for executions, a daemon that makes execution decisions
|
||||
based on the contents of a local database, a GUI agent that notifies the user in
|
||||
case of a block decision and a command-line utility for managing the system and
|
||||
@@ -48,9 +48,7 @@ disclosure reporting.
|
||||
the events database. In LOCKDOWN mode, only listed binaries are allowed to
|
||||
run.
|
||||
|
||||
* Event logging: When the kext is loaded, all binary launches are logged. When
|
||||
in either mode, all unknown or denied binaries are stored in the database to
|
||||
enable later aggregation.
|
||||
* Event logging: When the system extension is loaded, all binary launches are logged. When in either mode, all unknown or denied binaries are stored in the database to enable later aggregation.
|
||||
|
||||
* Certificate-based rules, with override levels: Instead of relying on a
|
||||
binary's hash (or 'fingerprint'), executables can be allowed/blocked by their
|
||||
@@ -140,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
|
||||
|
||||
@@ -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"],
|
||||
@@ -84,12 +84,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",
|
||||
@@ -103,7 +113,7 @@ objc_library(
|
||||
defines = ["SANTAGUI"],
|
||||
deps = [
|
||||
":SNTConfigurator",
|
||||
":SNTDeviceEvent",
|
||||
":SNTFileAccessEvent",
|
||||
":SNTLogging",
|
||||
":SNTStoredEvent",
|
||||
":SNTSystemInfo",
|
||||
@@ -133,6 +143,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"],
|
||||
@@ -225,7 +249,11 @@ objc_library(
|
||||
santa_unit_test(
|
||||
name = "SNTRuleTest",
|
||||
srcs = ["SNTRuleTest.m"],
|
||||
deps = [":SNTRule"],
|
||||
deps = [
|
||||
":SNTCommonEnums",
|
||||
":SNTRule",
|
||||
":SNTSyncConstants",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
@@ -380,10 +408,24 @@ santa_unit_test(
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTBlockMessageTest",
|
||||
srcs = ["SNTBlockMessageTest.m"],
|
||||
deps = [
|
||||
":SNTBlockMessage",
|
||||
":SNTConfigurator",
|
||||
":SNTFileAccessEvent",
|
||||
":SNTStoredEvent",
|
||||
":SNTSystemInfo",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "unit_tests",
|
||||
tests = [
|
||||
":PrefixTreeTest",
|
||||
":SNTBlockMessageTest",
|
||||
":SNTCachedDecisionTest",
|
||||
":SNTFileInfoTest",
|
||||
":SNTKVOManagerTest",
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -42,6 +42,7 @@
|
||||
@property NSString *quarantineURL;
|
||||
|
||||
@property NSString *customMsg;
|
||||
@property NSString *customURL;
|
||||
@property BOOL silentBlock;
|
||||
|
||||
@end
|
||||
|
||||
@@ -36,13 +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,
|
||||
SNTRuleTypeSigningID = 4,
|
||||
SNTRuleTypeBinary = 1000,
|
||||
SNTRuleTypeSigningID = 2000,
|
||||
SNTRuleTypeCertificate = 3000,
|
||||
SNTRuleTypeTeamID = 4000,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTRuleState) {
|
||||
@@ -114,6 +121,7 @@ typedef NS_ENUM(NSInteger, SNTEventLogType) {
|
||||
SNTEventLogTypeSyslog,
|
||||
SNTEventLogTypeFilelog,
|
||||
SNTEventLogTypeProtobuf,
|
||||
SNTEventLogTypeJSON,
|
||||
SNTEventLogTypeNull,
|
||||
};
|
||||
|
||||
@@ -144,6 +152,20 @@ typedef NS_ENUM(NSInteger, SNTMetricFormatType) {
|
||||
SNTMetricFormatTypeMonarchJSON,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTOverrideFileAccessAction) {
|
||||
SNTOverrideFileAccessActionNone,
|
||||
SNTOverrideFileAccessActionAuditOnly,
|
||||
SNTOverrideFileAccessActionDiable,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTDeviceManagerStartupPreferences) {
|
||||
SNTDeviceManagerStartupPreferencesNone,
|
||||
SNTDeviceManagerStartupPreferencesUnmount,
|
||||
SNTDeviceManagerStartupPreferencesForceUnmount,
|
||||
SNTDeviceManagerStartupPreferencesRemount,
|
||||
SNTDeviceManagerStartupPreferencesForceRemount,
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
enum class FileAccessPolicyDecision {
|
||||
kNoPolicy,
|
||||
|
||||
@@ -262,6 +262,16 @@
|
||||
///
|
||||
@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
|
||||
@@ -390,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.
|
||||
///
|
||||
@@ -423,6 +454,39 @@
|
||||
///
|
||||
@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.
|
||||
///
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
@@ -57,6 +56,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";
|
||||
@@ -101,6 +101,7 @@ static NSString *const kSpoolDirectoryEventMaxFlushTimeSec = @"SpoolDirectoryEve
|
||||
|
||||
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";
|
||||
@@ -120,6 +121,7 @@ static NSString *const kClientModeKey = @"ClientMode";
|
||||
static NSString *const kFailClosedKey = @"FailClosed";
|
||||
static NSString *const kBlockUSBMountKey = @"BlockUSBMount";
|
||||
static NSString *const kRemountUSBModeKey = @"RemountUSBMode";
|
||||
static NSString *const kOnStartUSBOptions = @"OnStartUSBOptions";
|
||||
static NSString *const kEnableTransitiveRulesKey = @"EnableTransitiveRules";
|
||||
static NSString *const kEnableTransitiveRulesKeyDeprecated = @"EnableTransitiveWhitelisting";
|
||||
static NSString *const kAllowedPathRegexKey = @"AllowedPathRegex";
|
||||
@@ -128,6 +130,7 @@ static NSString *const kBlockedPathRegexKey = @"BlockedPathRegex";
|
||||
static NSString *const kBlockedPathRegexKeyDeprecated = @"BlacklistRegex";
|
||||
static NSString *const kEnableAllEventUploadKey = @"EnableAllEventUpload";
|
||||
static NSString *const kDisableUnknownEventUploadKey = @"DisableUnknownEventUpload";
|
||||
static NSString *const kOverrideFileAccessActionKey = @"OverrideFileAccessAction";
|
||||
|
||||
static NSString *const kMetricFormat = @"MetricFormat";
|
||||
static NSString *const kMetricURL = @"MetricURL";
|
||||
@@ -164,6 +167,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kRuleSyncLastSuccess : date,
|
||||
kSyncCleanRequired : number,
|
||||
kEnableAllEventUploadKey : number,
|
||||
kOverrideFileAccessActionKey : string,
|
||||
};
|
||||
_forcedConfigKeyTypes = @{
|
||||
kClientModeKey : number,
|
||||
@@ -178,6 +182,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kBlockedPathRegexKeyDeprecated : re,
|
||||
kBlockUSBMountKey : number,
|
||||
kRemountUSBModeKey : array,
|
||||
kOnStartUSBOptions : string,
|
||||
kEnablePageZeroProtectionKey : number,
|
||||
kEnableBadSignatureProtectionKey : number,
|
||||
kEnableSilentModeKey : number,
|
||||
@@ -194,7 +199,9 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kModeNotificationLockdown : string,
|
||||
kStaticRules : array,
|
||||
kSyncBaseURLKey : string,
|
||||
kSyncEnableCleanSyncEventUpload : number,
|
||||
kSyncProxyConfigKey : dictionary,
|
||||
kSyncExtraHeadersKey : dictionary,
|
||||
kClientAuthCertificateFileKey : string,
|
||||
kClientAuthCertificatePasswordKey : string,
|
||||
kClientAuthCertificateCNKey : string,
|
||||
@@ -216,6 +223,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kSpoolDirectoryEventMaxFlushTimeSec : number,
|
||||
kFileAccessPolicy : dictionary,
|
||||
kFileAccessPolicyPlist : string,
|
||||
kFileAccessBlockMessage : string,
|
||||
kFileAccessPolicyUpdateIntervalSec : number,
|
||||
kEnableMachineIDDecoration : number,
|
||||
kEnableForkAndExitLogging : number,
|
||||
@@ -231,6 +239,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kMetricExtraLabels : dictionary,
|
||||
kEnableAllEventUploadKey : number,
|
||||
kDisableUnknownEventUploadKey : number,
|
||||
kOverrideFileAccessActionKey : string,
|
||||
};
|
||||
_defaults = [NSUserDefaults standardUserDefaults];
|
||||
[_defaults addSuiteNamed:@"com.google.santa"];
|
||||
@@ -314,6 +323,14 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingSyncExtraHeaders {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableCleanSyncEventUpload {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnablePageZeroProtection {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -430,6 +447,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFileAccessBlockMessage {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicyUpdateIntervalSec {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -502,6 +523,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingOverrideFileAccessActionKey {
|
||||
return [self syncAndConfigStateSet];
|
||||
}
|
||||
|
||||
#pragma mark Public Interface
|
||||
|
||||
- (SNTClientMode)clientMode {
|
||||
@@ -612,6 +637,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;
|
||||
}
|
||||
@@ -627,6 +668,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;
|
||||
@@ -794,6 +839,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 {
|
||||
@@ -844,6 +891,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)fileAccessBlockMessage {
|
||||
return self.configState[kFileAccessBlockMessage];
|
||||
}
|
||||
|
||||
- (uint32_t)fileAccessPolicyUpdateIntervalSec {
|
||||
return self.configState[kFileAccessPolicyUpdateIntervalSec]
|
||||
? [self.configState[kFileAccessPolicyUpdateIntervalSec] unsignedIntValue]
|
||||
@@ -924,6 +975,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.
|
||||
@@ -1056,7 +1134,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;
|
||||
|
||||
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
|
||||
@@ -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,20 +35,84 @@
|
||||
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;
|
||||
_customMsg = customMsg;
|
||||
_timestamp = timestamp;
|
||||
|
||||
if (_type == SNTRuleTypeBinary || _type == SNTRuleTypeCertificate) {
|
||||
NSCharacterSet *nonHex =
|
||||
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
|
||||
if ([[_identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length != 64)
|
||||
return nil;
|
||||
} else if (_identifier.length == 0) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -111,7 +182,14 @@
|
||||
customMsg = nil;
|
||||
}
|
||||
|
||||
return [self initWithIdentifier:identifier state:state type:type customMsg:customMsg];
|
||||
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
|
||||
@@ -131,6 +209,7 @@
|
||||
ENCODE(@(self.state), @"state");
|
||||
ENCODE(@(self.type), @"type");
|
||||
ENCODE(self.customMsg, @"custommsg");
|
||||
ENCODE(self.customURL, @"customurl");
|
||||
ENCODE(@(self.timestamp), @"timestamp");
|
||||
}
|
||||
|
||||
@@ -141,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
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
#include "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTSyncConstants.h"
|
||||
|
||||
#import "Source/common/SNTRule.h"
|
||||
|
||||
@@ -46,13 +48,25 @@
|
||||
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);
|
||||
|
||||
@@ -68,26 +82,114 @@
|
||||
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 {
|
||||
@@ -123,4 +225,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
|
||||
|
||||
@@ -54,6 +54,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;
|
||||
@@ -124,6 +125,7 @@ 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;
|
||||
@@ -135,6 +137,9 @@ extern NSString *const kLogSync;
|
||||
|
||||
extern const NSUInteger kDefaultEventBatchSize;
|
||||
|
||||
extern NSString *const kPostflightRulesReceived;
|
||||
extern NSString *const kPostflightRulesProcessed;
|
||||
|
||||
///
|
||||
/// kDefaultFullSyncInterval
|
||||
/// kDefaultFCMFullSyncInterval
|
||||
|
||||
@@ -47,6 +47,7 @@ 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";
|
||||
@@ -125,6 +126,7 @@ 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";
|
||||
@@ -134,6 +136,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;
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
reply:(void (^)(SNTRule *))reply;
|
||||
- (void)retrieveAllRules:(void (^)(NSArray<SNTRule *> *rules, NSError *error))reply;
|
||||
|
||||
///
|
||||
/// Config ops
|
||||
@@ -53,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
|
||||
|
||||
@@ -53,6 +53,11 @@ NSString *const kBundleID = @"com.google.santa.daemon";
|
||||
forSelector:@selector(databaseRuleAddRules:cleanSlate: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
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -30,6 +31,23 @@ 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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -381,6 +381,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
|
||||
@@ -429,6 +434,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
|
||||
@@ -526,6 +534,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",
|
||||
|
||||
@@ -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
|
||||
|
||||
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,6 +31,7 @@ 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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -114,6 +115,8 @@ santa_unit_test(
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SNTXPCBundleServiceInterface",
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
"@MOLCertificate",
|
||||
"@MOLCodesignChecker",
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/common/SNTXPCBundleServiceInterface.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/santactl/SNTCommand.h"
|
||||
#import "Source/santactl/SNTCommandController.h"
|
||||
@@ -55,6 +57,13 @@ static NSString *const kValidUntil = @"Valid Until";
|
||||
static NSString *const kSHA256 = @"SHA-256";
|
||||
static NSString *const kSHA1 = @"SHA-1";
|
||||
|
||||
// bundle info keys
|
||||
static NSString *const kBundleInfo = @"Bundle Info";
|
||||
static NSString *const kBundlePath = @"Main Bundle Path";
|
||||
static NSString *const kBundleID = @"Main Bundle ID";
|
||||
static NSString *const kBundleHash = @"Bundle Hash";
|
||||
static NSString *const kBundleHashes = @"Bundle Hashes";
|
||||
|
||||
// Message displayed when daemon communication fails
|
||||
static NSString *const kCommunicationErrorMsg = @"Could not communicate with daemon";
|
||||
|
||||
@@ -72,6 +81,7 @@ NSString *formattedStringForKeyArray(NSArray<NSString *> *array) {
|
||||
// Properties set from commandline flags
|
||||
@property(nonatomic) BOOL recursive;
|
||||
@property(nonatomic) BOOL jsonOutput;
|
||||
@property(nonatomic) BOOL bundleInfo;
|
||||
@property(nonatomic) NSNumber *certIndex;
|
||||
@property(nonatomic, copy) NSArray<NSString *> *outputKeyList;
|
||||
@property(nonatomic, copy) NSDictionary<NSString *, NSRegularExpression *> *outputFilters;
|
||||
@@ -156,6 +166,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
@"\n"
|
||||
@"Usage: santactl fileinfo [options] [file-paths]\n"
|
||||
@" --recursive (-r): Search directories recursively.\n"
|
||||
@" Incompatible with --bundleinfo.\n"
|
||||
@" --json: Output in JSON format.\n"
|
||||
@" --key: Search and return this one piece of information.\n"
|
||||
@" You may specify multiple keys by repeating this flag.\n"
|
||||
@@ -167,12 +178,16 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
@" signing chain to show info only for that certificate.\n"
|
||||
@" 0 up to n for the leaf certificate up to the root\n"
|
||||
@" -1 down to -n-1 for the root certificate down to the leaf\n"
|
||||
@" Incompatible with --bundleinfo."
|
||||
@"\n"
|
||||
@" --filter: Use predicates of the form 'key=regex' to filter out which files\n"
|
||||
@" are displayed. Valid keys are the same as for --key. Value is a\n"
|
||||
@" case-insensitive regular expression which must match anywhere in\n"
|
||||
@" the keyed property value for the file's info to be displayed.\n"
|
||||
@" You may specify multiple filters by repeating this flag.\n"
|
||||
@" --bundleinfo: If the file is part of a bundle, will also display bundle\n"
|
||||
@" hash information and hashes of all bundle executables.\n"
|
||||
@" Incompatible with --recursive and --cert-index.\n"
|
||||
@"\n"
|
||||
@"Examples: santactl fileinfo --cert-index 1 --key SHA-256 --json /usr/bin/yes\n"
|
||||
@" santactl fileinfo --key SHA-256 --json /usr/bin/yes\n"
|
||||
@@ -682,6 +697,48 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
if (outputDict[key]) continue; // ignore keys that we've already set due to a filter
|
||||
outputDict[key] = self.propertyMap[key](self, fileInfo);
|
||||
}
|
||||
|
||||
if (self.bundleInfo) {
|
||||
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
|
||||
se.fileBundlePath = fileInfo.bundlePath;
|
||||
|
||||
MOLXPCConnection *bc = [SNTXPCBundleServiceInterface configuredConnection];
|
||||
[bc resume];
|
||||
|
||||
__block NSMutableDictionary *bundleInfo = [[NSMutableDictionary alloc] init];
|
||||
|
||||
bundleInfo[kBundlePath] = fileInfo.bundle.bundlePath;
|
||||
bundleInfo[kBundleID] = fileInfo.bundle.bundleIdentifier;
|
||||
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
[[bc remoteObjectProxy]
|
||||
hashBundleBinariesForEvent:se
|
||||
reply:^(NSString *hash, NSArray<SNTStoredEvent *> *events,
|
||||
NSNumber *time) {
|
||||
bundleInfo[kBundleHash] = hash;
|
||||
|
||||
NSMutableArray *bundleHashes = [[NSMutableArray alloc] init];
|
||||
|
||||
for (SNTStoredEvent *event in events) {
|
||||
[bundleHashes
|
||||
addObject:@{kSHA256 : event.fileSHA256, kPath : event.filePath}];
|
||||
}
|
||||
|
||||
bundleInfo[kBundleHashes] = bundleHashes;
|
||||
[[bc remoteObjectProxy] spindown];
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
|
||||
int secondsToWait = 30;
|
||||
if (dispatch_semaphore_wait(sema,
|
||||
dispatch_time(DISPATCH_TIME_NOW, secondsToWait * NSEC_PER_SEC))) {
|
||||
fprintf(stderr, "The bundle service did not finish collecting hashes within %d seconds\n",
|
||||
secondsToWait);
|
||||
}
|
||||
|
||||
outputDict[kBundleInfo] = bundleInfo;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's nothing in the outputDict, then don't need to print anything.
|
||||
@@ -710,6 +767,11 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (self.bundleInfo) {
|
||||
[output appendString:[self stringForBundleInfo:outputDict[kBundleInfo] key:kBundleInfo]];
|
||||
}
|
||||
|
||||
if (!singleKey) [output appendString:@"\n"];
|
||||
}
|
||||
|
||||
@@ -739,6 +801,9 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
if ([arg caseInsensitiveCompare:@"--json"] == NSOrderedSame) {
|
||||
self.jsonOutput = YES;
|
||||
} else if ([arg caseInsensitiveCompare:@"--cert-index"] == NSOrderedSame) {
|
||||
if (self.bundleInfo) {
|
||||
[self printErrorUsageAndExit:@"\n--cert-index is incompatible with --bundleinfo"];
|
||||
}
|
||||
i += 1; // advance to next argument and grab index
|
||||
if (i >= nargs || [arguments[i] hasPrefix:@"--"]) {
|
||||
[self printErrorUsageAndExit:@"\n--cert-index requires an argument"];
|
||||
@@ -788,7 +853,17 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
filters[key] = regex;
|
||||
} else if ([arg caseInsensitiveCompare:@"--recursive"] == NSOrderedSame ||
|
||||
[arg caseInsensitiveCompare:@"-r"] == NSOrderedSame) {
|
||||
if (self.bundleInfo) {
|
||||
[self printErrorUsageAndExit:@"\n--recursive is incompatible with --bundleinfo"];
|
||||
}
|
||||
self.recursive = YES;
|
||||
} else if ([arg caseInsensitiveCompare:@"--bundleinfo"] == NSOrderedSame ||
|
||||
[arg caseInsensitiveCompare:@"-b"] == NSOrderedSame) {
|
||||
if (self.recursive || self.certIndex) {
|
||||
[self printErrorUsageAndExit:
|
||||
@"\n--bundleinfo is incompatible with --recursive and --cert-index"];
|
||||
}
|
||||
self.bundleInfo = YES;
|
||||
} else {
|
||||
[paths addObject:arg];
|
||||
}
|
||||
@@ -868,6 +943,22 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
- (NSString *)stringForBundleInfo:(NSDictionary *)bundleInfo key:(NSString *)key {
|
||||
NSMutableString *result = [NSMutableString string];
|
||||
|
||||
[result appendFormat:@"%@:\n", key];
|
||||
|
||||
[result appendFormat:@" %-20s: %@\n", kBundlePath.UTF8String, bundleInfo[kBundlePath]];
|
||||
[result appendFormat:@" %-20s: %@\n", kBundleID.UTF8String, bundleInfo[kBundleID]];
|
||||
[result appendFormat:@" %-20s: %@\n", kBundleHash.UTF8String, bundleInfo[kBundleHash]];
|
||||
|
||||
for (NSDictionary *hashPath in bundleInfo[kBundleHashes]) {
|
||||
[result appendFormat:@" %@ %@\n", hashPath[kSHA256], hashPath[kPath]];
|
||||
}
|
||||
|
||||
return [result copy];
|
||||
}
|
||||
|
||||
- (NSString *)stringForCertificate:(NSDictionary *)cert withKeys:(NSArray *)keys index:(int)index {
|
||||
if (!cert) return @"";
|
||||
NSMutableString *result = [NSMutableString string];
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
@" --compiler: allow and mark as a compiler\n"
|
||||
@" --remove: remove existing rule\n"
|
||||
@" --check: check for an existing rule\n"
|
||||
@" --import {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"
|
||||
@@ -81,7 +83,21 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
@" 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");
|
||||
@" 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 {
|
||||
@@ -103,7 +119,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) {
|
||||
@@ -154,11 +173,48 @@ 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 (jsonFilePath.length > 0) {
|
||||
if (importRules) {
|
||||
if (newRule.identifier != nil || path != nil || check) {
|
||||
[self printErrorUsageAndExit:@"--import can only be used by itself"];
|
||||
}
|
||||
[self importJSONFile:jsonFilePath];
|
||||
} else if (exportRules) {
|
||||
if (newRule.identifier != nil || path != nil || check) {
|
||||
[self printErrorUsageAndExit:@"--export can only be used by itself"];
|
||||
}
|
||||
[self exportJSONFile:jsonFilePath];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (path) {
|
||||
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithPath:path];
|
||||
if (!fi.path) {
|
||||
@@ -302,4 +358,101 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
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
|
||||
cleanSlate:NO
|
||||
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 relevan.
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -195,6 +206,7 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"remount_usb_mode" : (configurator.blockUSBMount && configurator.remountUSBMode.count
|
||||
? configurator.remountUSBMode
|
||||
: @""),
|
||||
@"on_start_usb_options" : StartupOptionToString(configurator.onStartUSBOptions),
|
||||
},
|
||||
@"database" : @{
|
||||
@"binary_rules" : @(binaryRuleCount),
|
||||
@@ -252,9 +264,11 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
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:",
|
||||
printf(" %-25s | %s\n", "USB Remounting Mode",
|
||||
[[configurator.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);
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ REGISTER_COMMAND_NAME(@"sync")
|
||||
#pragma mark SNTCommand protocol methods
|
||||
|
||||
+ (BOOL)requiresRoot {
|
||||
return YES;
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (BOOL)requiresDaemonConn {
|
||||
|
||||
@@ -209,6 +209,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"],
|
||||
@@ -221,6 +234,7 @@ objc_library(
|
||||
":SNTPolicyProcessor",
|
||||
":SNTRuleTable",
|
||||
":SNTSyncdQueue",
|
||||
":TTYWriter",
|
||||
"//Source/common:BranchPrediction",
|
||||
"//Source/common:SNTBlockMessage",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
@@ -286,7 +300,9 @@ objc_library(
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:String",
|
||||
"//Source/common:Unit",
|
||||
],
|
||||
)
|
||||
@@ -332,6 +348,9 @@ objc_library(
|
||||
name = "SNTEndpointSecurityFileAccessAuthorizer",
|
||||
srcs = ["EventProviders/SNTEndpointSecurityFileAccessAuthorizer.mm"],
|
||||
hdrs = ["EventProviders/SNTEndpointSecurityFileAccessAuthorizer.h"],
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
":EndpointSecurityAPI",
|
||||
":EndpointSecurityEnrichedTypes",
|
||||
@@ -343,18 +362,24 @@ objc_library(
|
||||
":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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -370,8 +395,10 @@ objc_library(
|
||||
":Metrics",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTDeviceEvent",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -449,6 +476,7 @@ objc_library(
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
"//Source/common:String",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -506,6 +534,8 @@ objc_library(
|
||||
"//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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -646,6 +676,7 @@ objc_library(
|
||||
hdrs = ["Metrics.h"],
|
||||
deps = [
|
||||
":SNTApplicationCoreMetrics",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTXPCMetricServiceInterface",
|
||||
@@ -674,10 +705,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",
|
||||
@@ -705,6 +738,7 @@ objc_library(
|
||||
":SNTNotificationQueue",
|
||||
":SNTRuleTable",
|
||||
":SNTSyncdQueue",
|
||||
":TTYWriter",
|
||||
":WatchItems",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTConfigurator",
|
||||
@@ -861,8 +895,10 @@ santa_unit_test(
|
||||
":Metrics",
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTDatabaseController",
|
||||
":SNTDecisionCache",
|
||||
":SNTEndpointSecurityAuthorizer",
|
||||
":SantadDeps",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:TestUtils",
|
||||
"@MOLCertificate",
|
||||
@@ -961,7 +997,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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1210,6 +1248,7 @@ santa_unit_test(
|
||||
":WatchItems",
|
||||
"//Source/common:Platform",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:TestUtils",
|
||||
"@MOLCertificate",
|
||||
@@ -1257,6 +1296,7 @@ santa_unit_test(
|
||||
":SNTCompilerController",
|
||||
":SNTEndpointSecurityRecorder",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:TestUtils",
|
||||
"//Source/common:Unit",
|
||||
"@OCMock",
|
||||
@@ -1279,6 +1319,7 @@ santa_unit_test(
|
||||
":Metrics",
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTEndpointSecurityDeviceManager",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDeviceEvent",
|
||||
"//Source/common:TestUtils",
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
}
|
||||
[self closeDeleteReopenDatabase:db];
|
||||
} else if ([db userVersion] > [self currentSupportedVersion]) {
|
||||
LOGW(@"Database version newer than supported. Deleting.");
|
||||
[self closeDeleteReopenDatabase:db];
|
||||
}
|
||||
}];
|
||||
|
||||
@@ -101,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,7 +25,7 @@
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
|
||||
static const uint32_t kRuleTableCurrentVersion = 4;
|
||||
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.
|
||||
@@ -210,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];
|
||||
@@ -236,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 {
|
||||
@@ -271,27 +306,21 @@ 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 {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=4"];
|
||||
}];
|
||||
return count;
|
||||
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
|
||||
@@ -347,10 +376,10 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
//
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
FMResultSet *rs = [db executeQuery:@"SELECT * FROM rules WHERE "
|
||||
@" (identifier=? and type=1) "
|
||||
@"OR (identifier=? AND type=4) "
|
||||
@"OR (identifier=? AND type=2) "
|
||||
@"OR (identifier=? AND type=3) LIMIT 1",
|
||||
@" (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];
|
||||
@@ -405,10 +434,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]];
|
||||
@@ -510,4 +539,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,7 +36,7 @@
|
||||
|
||||
- (SNTRule *)_exampleTeamIDRule {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
r.identifier = @"teamID";
|
||||
r.identifier = @"ABCDEFGHIJ";
|
||||
r.state = SNTRuleStateBlock;
|
||||
r.type = SNTRuleTypeTeamID;
|
||||
r.customMsg = @"A teamID rule";
|
||||
@@ -48,11 +48,11 @@
|
||||
if (isPlatformBinary) {
|
||||
r.identifier = @"platform:signingID";
|
||||
} else {
|
||||
r.identifier = @"teamID:signingID";
|
||||
r.identifier = @"ABCDEFGHIJ:signingID";
|
||||
}
|
||||
r.state = SNTRuleStateBlock;
|
||||
r.type = SNTRuleTypeSigningID;
|
||||
r.customMsg = @"A teamID rule";
|
||||
r.customMsg = @"A signingID rule";
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -187,9 +187,9 @@
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@"teamID"];
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID");
|
||||
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeTeamID);
|
||||
XCTAssertEqual([self.sut teamIDRuleCount], 1);
|
||||
|
||||
@@ -211,12 +211,12 @@
|
||||
XCTAssertEqual([self.sut signingIDRuleCount], 2);
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:@"teamID:signingID"
|
||||
signingID:@"ABCDEFGHIJ:signingID"
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID:signingID");
|
||||
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil
|
||||
@@ -243,9 +243,9 @@
|
||||
// See the comment in SNTRuleTable#ruleForBinarySHA256:certificateSHA256:teamID
|
||||
SNTRule *r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:@"teamID:signingID"
|
||||
signingID:@"ABCDEFGHIJ:signingID"
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:@"teamID"];
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
@@ -253,9 +253,9 @@
|
||||
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:@"teamID:signingID"
|
||||
signingID:@"ABCDEFGHIJ:signingID"
|
||||
certificateSHA256:@"unknowncert"
|
||||
teamID:@"teamID"];
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
@@ -265,26 +265,26 @@
|
||||
ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"unknown"
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:@"teamID"];
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCertificate, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"teamID:signingID"
|
||||
signingID:@"ABCDEFGHIJ:signingID"
|
||||
certificateSHA256:@"unknown"
|
||||
teamID:@"teamID"];
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID:signingID");
|
||||
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID, @"Implicit rule ordering failed (SigningID)");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"unknown"
|
||||
certificateSHA256:@"unknown"
|
||||
teamID:@"teamID"];
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID");
|
||||
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeTeamID, @"Implicit rule ordering failed (TeamID)");
|
||||
}
|
||||
|
||||
@@ -301,4 +301,25 @@
|
||||
[[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]
|
||||
]
|
||||
cleanSlate:NO
|
||||
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,16 +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),
|
||||
@@ -47,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); }
|
||||
@@ -71,27 +71,34 @@ struct WatchItemPolicy {
|
||||
bool ara = kWatchItemPolicyDefaultAllowReadAccess,
|
||||
bool ao = kWatchItemPolicyDefaultAuditOnly,
|
||||
bool ipe = kWatchItemPolicyDefaultInvertProcessExceptions,
|
||||
std::vector<Process> procs = {})
|
||||
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 &&
|
||||
invert_process_exceptions == other.invert_process_exceptions &&
|
||||
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;
|
||||
@@ -99,7 +106,15 @@ struct WatchItemPolicy {
|
||||
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
|
||||
|
||||
@@ -40,6 +40,9 @@ 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;
|
||||
@@ -93,6 +96,9 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
|
||||
|
||||
std::optional<WatchItemsState> State();
|
||||
|
||||
std::pair<NSString *, NSString *> EventDetailLinkInfo(
|
||||
const std::shared_ptr<WatchItemPolicy> &watch_item);
|
||||
|
||||
friend class santa::santad::data_layer::WatchItemsPeer;
|
||||
|
||||
private:
|
||||
@@ -125,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
|
||||
|
||||
@@ -46,6 +46,8 @@ 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";
|
||||
@@ -54,6 +56,11 @@ 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";
|
||||
@@ -73,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
|
||||
@@ -126,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) {
|
||||
@@ -140,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) {
|
||||
@@ -379,6 +406,16 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
|
||||
/// <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>
|
||||
@@ -405,31 +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, kWatchItemConfigKeyOptionsInvertProcessExceptions,
|
||||
[NSNumber class], err)) {
|
||||
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 =
|
||||
options[kWatchItemConfigKeyOptionsInvertProcessExceptions]
|
||||
? [options[kWatchItemConfigKeyOptionsInvertProcessExceptions] boolValue]
|
||||
: kWatchItemPolicyDefaultInvertProcessExceptions;
|
||||
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)) {
|
||||
@@ -439,7 +493,11 @@ 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>(
|
||||
NSStringToUTF8StringView(name), path_type_pair.first, path_type_pair.second,
|
||||
allow_read_access, audit_only, invert_process_exceptions, std::get<ProcessList>(proc_list)));
|
||||
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;
|
||||
@@ -487,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",
|
||||
@@ -661,8 +729,18 @@ void WatchItems::UpdateCurrentState(
|
||||
current_config_ = new_config;
|
||||
if (new_config) {
|
||||
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];
|
||||
@@ -794,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
|
||||
|
||||
@@ -93,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;
|
||||
@@ -757,37 +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));
|
||||
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsInvertProcessExceptions : @""}
|
||||
},
|
||||
policies, &err));
|
||||
XCTAssertTrue(ParseConfigSingleWatchItem(@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsInvertProcessExceptions : @(0)}
|
||||
},
|
||||
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`
|
||||
@@ -806,7 +831,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
*policies[0].get(),
|
||||
WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType,
|
||||
kWatchItemPolicyDefaultAllowReadAccess, kWatchItemPolicyDefaultAuditOnly,
|
||||
kWatchItemPolicyDefaultInvertProcessExceptions, {}));
|
||||
kWatchItemPolicyDefaultInvertProcessExceptions));
|
||||
|
||||
// Test multiple paths, options, and processes
|
||||
policies.clear();
|
||||
@@ -822,6 +847,9 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
kWatchItemConfigKeyOptionsAllowReadAccess : @(YES),
|
||||
kWatchItemConfigKeyOptionsAuditOnly : @(NO),
|
||||
kWatchItemConfigKeyOptionsInvertProcessExceptions : @(YES),
|
||||
kWatchItemConfigKeyOptionsEnableSilentMode : @(YES),
|
||||
kWatchItemConfigKeyOptionsEnableSilentMode : @(NO),
|
||||
kWatchItemConfigKeyOptionsCustomMessage : @"",
|
||||
},
|
||||
kWatchItemConfigKeyProcesses : @[
|
||||
@{kWatchItemConfigKeyProcessesBinaryPath : @"pa"},
|
||||
@@ -829,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, true, procs));
|
||||
XCTAssertEqual(*policies[1].get(), WatchItemPolicy("rule", "b", WatchItemPathType::kPrefix, true,
|
||||
false, true, procs));
|
||||
XCTAssertEqual(*policies[0].get(),
|
||||
WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType, true, false, true,
|
||||
true, false, "", nil, nil, procs));
|
||||
XCTAssertEqual(*policies[1].get(),
|
||||
WatchItemPolicy("rule", "b", WatchItemPathType::kPrefix, true, false, true, true,
|
||||
false, "", nil, nil, procs));
|
||||
}
|
||||
|
||||
- (void)testState {
|
||||
|
||||
@@ -60,7 +60,8 @@ NSString *const FlushCacheReasonToString(FlushCacheReason reason) {
|
||||
case FlushCacheReason::kExplicitCommand: return kFlushCacheReasonExplicitCommand;
|
||||
case FlushCacheReason::kFilesystemUnmounted: return kFlushCacheReasonFilesystemUnmounted;
|
||||
default:
|
||||
[NSException raise:@"Invalid reason" format:@"Unknown reason value: %d", reason];
|
||||
[NSException raise:@"Invalid reason"
|
||||
format:@"Unknown reason value: %d", static_cast<int>(reason)];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -320,9 +320,19 @@ class EnrichedUnlink : public EnrichedEventType {
|
||||
EnrichedFile target_;
|
||||
};
|
||||
|
||||
class EnrichedCSInvalidated : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedCSInvalidated(Message &&es_msg, EnrichedProcess &&instigator)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)) {}
|
||||
EnrichedCSInvalidated(EnrichedCSInvalidated &&other)
|
||||
: EnrichedEventType(std::move(other)) {}
|
||||
EnrichedCSInvalidated(const EnrichedCSInvalidated &other) = delete;
|
||||
};
|
||||
|
||||
using EnrichedType =
|
||||
std::variant<EnrichedClose, EnrichedExchange, EnrichedExec, EnrichedExit,
|
||||
EnrichedFork, EnrichedLink, EnrichedRename, EnrichedUnlink>;
|
||||
EnrichedFork, EnrichedLink, EnrichedRename, EnrichedUnlink,
|
||||
EnrichedCSInvalidated>;
|
||||
|
||||
class EnrichedMessage {
|
||||
public:
|
||||
|
||||
@@ -74,6 +74,9 @@ std::unique_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK:
|
||||
return std::make_unique<EnrichedMessage>(EnrichedUnlink(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.unlink.target)));
|
||||
case ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED:
|
||||
return std::make_unique<EnrichedMessage>(
|
||||
EnrichedCSInvalidated(std::move(es_msg), Enrich(*es_msg->process)));
|
||||
default:
|
||||
// This is a programming error
|
||||
LOGE(@"Attempting to enrich an unhandled event type: %d", es_msg->event_type);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <DiskArbitration/DiskArbitration.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#import "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
@@ -39,11 +40,16 @@ typedef void (^SNTDeviceBlockCallback)(SNTDeviceEvent *event);
|
||||
@property(nonatomic, nullable) SNTDeviceBlockCallback deviceBlockCallback;
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
|
||||
authResultCache:(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache;
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)
|
||||
esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
|
||||
authResultCache:
|
||||
(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache
|
||||
blockUSBMount:(BOOL)blockUSBMount
|
||||
remountUSBMode:(nullable NSArray<NSString *> *)remountUSBMode
|
||||
startupPreferences:(SNTDeviceManagerStartupPreferences)startupPrefs;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -24,9 +24,12 @@
|
||||
#include <errno.h>
|
||||
#include <libproc.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/ucred.h>
|
||||
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
@@ -38,32 +41,53 @@ using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
|
||||
// Defined operations for startup metrics:
|
||||
// Device shouldn't be operated on (e.g. not a mass storage device)
|
||||
static NSString *const kMetricStartupDiskOperationSkip = @"Skipped";
|
||||
// Device already had appropriate flags set
|
||||
static NSString *const kMetricStartupDiskOperationAllowed = @"Allowed";
|
||||
// Device failed to be unmounted
|
||||
static NSString *const kMetricStartupDiskOperationUnmountFailed = @"UnmountFailed";
|
||||
// Device failed to be remounted
|
||||
static NSString *const kMetricStartupDiskOperationRemountFailed = @"RemountFailed";
|
||||
// Remounts were requested, but remount args weren't set
|
||||
static NSString *const kMetricStartupDiskOperationRemountSkipped = @"RemountSkipped";
|
||||
// Operations on device matching the configured startup pref wwere successful
|
||||
static NSString *const kMetricStartupDiskOperationSuccess = @"Success";
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManager ()
|
||||
|
||||
- (void)logDiskAppeared:(NSDictionary *)props;
|
||||
- (void)logDiskDisappeared:(NSDictionary *)props;
|
||||
|
||||
@property SNTMetricCounter *startupDiskMetrics;
|
||||
@property DASessionRef diskArbSession;
|
||||
@property(nonatomic, readonly) dispatch_queue_t diskQueue;
|
||||
@property dispatch_semaphore_t diskSema;
|
||||
|
||||
@end
|
||||
|
||||
void diskMountedCallback(DADiskRef disk, DADissenterRef dissenter, void *context) {
|
||||
void DiskMountedCallback(DADiskRef disk, DADissenterRef dissenter, void *context) {
|
||||
if (dissenter) {
|
||||
DAReturn status = DADissenterGetStatus(dissenter);
|
||||
|
||||
NSString *statusString = (NSString *)DADissenterGetStatusString(dissenter);
|
||||
IOReturn systemCode = err_get_system(status);
|
||||
IOReturn subSystemCode = err_get_sub(status);
|
||||
IOReturn errorCode = err_get_code(status);
|
||||
|
||||
LOGE(@"SNTEndpointSecurityDeviceManager: dissenter status codes: system: %d, subsystem: %d, "
|
||||
@"err: %d; status: %s",
|
||||
systemCode, subSystemCode, errorCode, [statusString UTF8String]);
|
||||
@"err: %d; status: %@",
|
||||
systemCode, subSystemCode, errorCode,
|
||||
CFBridgingRelease(DADissenterGetStatusString(dissenter)));
|
||||
}
|
||||
|
||||
if (context) {
|
||||
dispatch_semaphore_t sema = (__bridge dispatch_semaphore_t)context;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}
|
||||
}
|
||||
|
||||
void diskAppearedCallback(DADiskRef disk, void *context) {
|
||||
void DiskAppearedCallback(DADiskRef disk, void *context) {
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
SNTEndpointSecurityDeviceManager *dm = (__bridge SNTEndpointSecurityDeviceManager *)context;
|
||||
@@ -71,7 +95,7 @@ void diskAppearedCallback(DADiskRef disk, void *context) {
|
||||
[dm logDiskAppeared:props];
|
||||
}
|
||||
|
||||
void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *context) {
|
||||
void DiskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *context) {
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
|
||||
@@ -82,7 +106,7 @@ void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *conte
|
||||
}
|
||||
}
|
||||
|
||||
void diskDisappearedCallback(DADiskRef disk, void *context) {
|
||||
void DiskDisappearedCallback(DADiskRef disk, void *context) {
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
|
||||
@@ -91,7 +115,22 @@ void diskDisappearedCallback(DADiskRef disk, void *context) {
|
||||
[dm logDiskDisappeared:props];
|
||||
}
|
||||
|
||||
NSArray<NSString *> *maskToMountArgs(long remountOpts) {
|
||||
void DiskUnmountCallback(DADiskRef disk, DADissenterRef dissenter, void *context) {
|
||||
if (dissenter) {
|
||||
LOGW(@"Unable to unmount device: %@", CFBridgingRelease(DADissenterGetStatusString(dissenter)));
|
||||
} else if (disk) {
|
||||
NSDictionary *diskInfo = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
LOGI(@"Unmounted device: Model: %@, Vendor: %@, Path: %@",
|
||||
diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceModelKey],
|
||||
diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceVendorKey],
|
||||
diskInfo[(__bridge NSString *)kDADiskDescriptionVolumePathKey]);
|
||||
}
|
||||
|
||||
dispatch_semaphore_t sema = (__bridge dispatch_semaphore_t)context;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}
|
||||
|
||||
NSArray<NSString *> *maskToMountArgs(uint32_t remountOpts) {
|
||||
NSMutableArray<NSString *> *args = [NSMutableArray array];
|
||||
if (remountOpts & MNT_RDONLY) [args addObject:@"rdonly"];
|
||||
if (remountOpts & MNT_NOEXEC) [args addObject:@"noexec"];
|
||||
@@ -104,28 +143,29 @@ NSArray<NSString *> *maskToMountArgs(long remountOpts) {
|
||||
return args;
|
||||
}
|
||||
|
||||
long mountArgsToMask(NSArray<NSString *> *args) {
|
||||
long flags = 0;
|
||||
uint32_t mountArgsToMask(NSArray<NSString *> *args) {
|
||||
uint32_t flags = 0;
|
||||
for (NSString *i in args) {
|
||||
NSString *arg = [i lowercaseString];
|
||||
if ([arg isEqualToString:@"rdonly"])
|
||||
if ([arg isEqualToString:@"rdonly"]) {
|
||||
flags |= MNT_RDONLY;
|
||||
else if ([arg isEqualToString:@"noexec"])
|
||||
} else if ([arg isEqualToString:@"noexec"]) {
|
||||
flags |= MNT_NOEXEC;
|
||||
else if ([arg isEqualToString:@"nosuid"])
|
||||
} else if ([arg isEqualToString:@"nosuid"]) {
|
||||
flags |= MNT_NOSUID;
|
||||
else if ([arg isEqualToString:@"nobrowse"])
|
||||
} else if ([arg isEqualToString:@"nobrowse"]) {
|
||||
flags |= MNT_DONTBROWSE;
|
||||
else if ([arg isEqualToString:@"noowners"])
|
||||
} else if ([arg isEqualToString:@"noowners"]) {
|
||||
flags |= MNT_UNKNOWNPERMISSIONS;
|
||||
else if ([arg isEqualToString:@"nodev"])
|
||||
} else if ([arg isEqualToString:@"nodev"]) {
|
||||
flags |= MNT_NODEV;
|
||||
else if ([arg isEqualToString:@"-j"])
|
||||
} else if ([arg isEqualToString:@"-j"]) {
|
||||
flags |= MNT_JOURNALED;
|
||||
else if ([arg isEqualToString:@"async"])
|
||||
} else if ([arg isEqualToString:@"async"]) {
|
||||
flags |= MNT_ASYNC;
|
||||
else
|
||||
} else {
|
||||
LOGE(@"SNTEndpointSecurityDeviceManager: unexpected mount arg: %@", arg);
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
@@ -140,25 +180,205 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
logger:(std::shared_ptr<Logger>)logger
|
||||
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache {
|
||||
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache
|
||||
blockUSBMount:(BOOL)blockUSBMount
|
||||
remountUSBMode:(nullable NSArray<NSString *> *)remountUSBMode
|
||||
startupPreferences:(SNTDeviceManagerStartupPreferences)startupPrefs {
|
||||
self = [super initWithESAPI:std::move(esApi)
|
||||
metrics:std::move(metrics)
|
||||
processor:santa::santad::Processor::kDeviceManager];
|
||||
if (self) {
|
||||
_logger = logger;
|
||||
_authResultCache = authResultCache;
|
||||
_blockUSBMount = false;
|
||||
_blockUSBMount = blockUSBMount;
|
||||
_remountArgs = remountUSBMode;
|
||||
|
||||
_diskQueue = dispatch_queue_create("com.google.santa.daemon.disk_queue", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
_diskArbSession = DASessionCreate(NULL);
|
||||
DASessionSetDispatchQueue(_diskArbSession, _diskQueue);
|
||||
|
||||
SNTMetricInt64Gauge *startupPrefsMetric =
|
||||
[[SNTMetricSet sharedInstance] int64GaugeWithName:@"/santa/device_manager/startup_preference"
|
||||
fieldNames:@[]
|
||||
helpText:@"The current startup preference value"];
|
||||
|
||||
[[SNTMetricSet sharedInstance] registerCallback:^{
|
||||
[startupPrefsMetric set:startupPrefs forFieldValues:@[]];
|
||||
}];
|
||||
|
||||
_startupDiskMetrics = [[SNTMetricSet sharedInstance]
|
||||
counterWithName:@"/santa/device_manager/startup_disk_operation"
|
||||
fieldNames:@[ @"operation" ]
|
||||
helpText:@"Count of the number of USB devices encountered per operation"];
|
||||
|
||||
[self performStartupTasks:startupPrefs];
|
||||
|
||||
[self establishClientOrDie];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (uint32_t)updatedMountFlags:(struct statfs *)sfs {
|
||||
uint32_t mask = sfs->f_flags | mountArgsToMask(self.remountArgs);
|
||||
|
||||
// NB: APFS mounts get MNT_JOURNALED implicitly set. However, mount_apfs
|
||||
// does not support the `-j` option so this flag needs to be cleared.
|
||||
if (strncmp(sfs->f_fstypename, "apfs", sizeof(sfs->f_fstypename)) == 0) {
|
||||
mask &= ~MNT_JOURNALED;
|
||||
}
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
- (BOOL)shouldOperateOnDisk:(DADiskRef)disk {
|
||||
NSDictionary *diskInfo = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
|
||||
BOOL isInternal = [diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceInternalKey] boolValue];
|
||||
BOOL isRemovable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaRemovableKey] boolValue];
|
||||
BOOL isEjectable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaEjectableKey] boolValue];
|
||||
NSString *protocol = diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey];
|
||||
BOOL isUSB = [protocol isEqualToString:@"USB"];
|
||||
BOOL isSecureDigital = [protocol isEqualToString:@"Secure Digital"];
|
||||
BOOL isVirtual = [protocol isEqualToString:@"Virtual Interface"];
|
||||
|
||||
NSString *kind = diskInfo[(__bridge NSString *)kDADiskDescriptionMediaKindKey];
|
||||
|
||||
// TODO: check kind and protocol for banned things (e.g. MTP).
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d "
|
||||
@"isRemovable: %d isEjectable: %d",
|
||||
protocol, kind, isInternal, isRemovable, isEjectable);
|
||||
|
||||
// if the device is internal, or virtual *AND* is not an SD Card,
|
||||
// then allow the mount. This is to ensure we block SD cards inserted into
|
||||
// the internal reader of some Macs, whilst also ensuring we don't block
|
||||
// the internal storage device.
|
||||
if ((isInternal || isVirtual) && !isSecureDigital) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We are okay with operations for devices that are non-removable as long as
|
||||
// they are NOT a USB device, or an SD Card.
|
||||
if (!isRemovable && !isEjectable && !isUSB && !isSecureDigital) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
- (BOOL)haveRemountArgs {
|
||||
return [self.remountArgs count] > 0;
|
||||
}
|
||||
|
||||
- (BOOL)remountUSBModeContainsFlags:(uint32_t)flags {
|
||||
if (![self haveRemountArgs]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t requiredFlags = mountArgsToMask(self.remountArgs);
|
||||
|
||||
LOGD(@" Got mount flags: 0x%08x | %@", flags, maskToMountArgs(flags));
|
||||
LOGD(@"Want mount flags: 0x%08x | %@", mountArgsToMask(self.remountArgs), self.remountArgs);
|
||||
|
||||
return (flags & requiredFlags) == requiredFlags;
|
||||
}
|
||||
|
||||
- (void)incrementStartupMetricsOperation:(NSString *)op {
|
||||
[self.startupDiskMetrics incrementForFieldValues:@[ op ]];
|
||||
}
|
||||
|
||||
// NB: Remount options are implemented as separate "unmount" and "mount"
|
||||
// operations instead of using the "update"/MNT_UPDATE flag. This is because
|
||||
// filesystems often don't support many transitions (e.g. RW to RO). Performing
|
||||
// the two step process has a higher chance of succeeding.
|
||||
- (void)performStartupTasks:(SNTDeviceManagerStartupPreferences)startupPrefs {
|
||||
if (!self.blockUSBMount || (startupPrefs != SNTDeviceManagerStartupPreferencesUnmount &&
|
||||
startupPrefs != SNTDeviceManagerStartupPreferencesForceUnmount &&
|
||||
startupPrefs != SNTDeviceManagerStartupPreferencesRemount &&
|
||||
startupPrefs != SNTDeviceManagerStartupPreferencesForceRemount)) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct statfs *mnts;
|
||||
int numMounts = getmntinfo_r_np(&mnts, MNT_WAIT);
|
||||
|
||||
if (numMounts == 0) {
|
||||
LOGE(@"Failed to get mount info: %d: %s", errno, strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
self.diskSema = dispatch_semaphore_create(0);
|
||||
|
||||
for (int i = 0; i < numMounts; i++) {
|
||||
struct statfs *sfs = &mnts[i];
|
||||
|
||||
DADiskRef disk = DADiskCreateFromBSDName(NULL, self.diskArbSession, sfs->f_mntfromname);
|
||||
if (!disk) {
|
||||
LOGW(@"Unable to create disk reference for device: '%s' -> '%s'", sfs->f_mntfromname,
|
||||
sfs->f_mntonname);
|
||||
continue;
|
||||
}
|
||||
|
||||
CFAutorelease(disk);
|
||||
|
||||
if (![self shouldOperateOnDisk:disk]) {
|
||||
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationSkip];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ([self remountUSBModeContainsFlags:sfs->f_flags]) {
|
||||
LOGI(@"Allowing existing mount as flags contain RemountUSBMode. '%s' -> '%s'",
|
||||
sfs->f_mntfromname, sfs->f_mntonname);
|
||||
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationAllowed];
|
||||
continue;
|
||||
}
|
||||
|
||||
DADiskUnmountOptions unmountOptions = kDADiskUnmountOptionDefault;
|
||||
if (startupPrefs == SNTDeviceManagerStartupPreferencesForceUnmount ||
|
||||
startupPrefs == SNTDeviceManagerStartupPreferencesForceRemount) {
|
||||
unmountOptions = kDADiskUnmountOptionForce;
|
||||
}
|
||||
|
||||
LOGI(@"Attempting to unmount device: '%s' mounted on '%s'", sfs->f_mntfromname,
|
||||
sfs->f_mntonname);
|
||||
|
||||
DADiskUnmount(disk, unmountOptions, DiskUnmountCallback, (__bridge void *)self.diskSema);
|
||||
|
||||
if (dispatch_semaphore_wait(self.diskSema,
|
||||
dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
|
||||
LOGW(
|
||||
@"Unmounting '%s' mounted on '%s' took longer than expected. Device may still be mounted.",
|
||||
sfs->f_mntfromname, sfs->f_mntonname);
|
||||
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationUnmountFailed];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (startupPrefs == SNTDeviceManagerStartupPreferencesRemount ||
|
||||
startupPrefs == SNTDeviceManagerStartupPreferencesForceRemount) {
|
||||
if (![self haveRemountArgs]) {
|
||||
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationRemountSkipped];
|
||||
LOGW(@"Remount requested during startup, but no remount args set. Leaving unmounted.");
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t newMode = [self updatedMountFlags:sfs];
|
||||
LOGI(@"Attempting to mount device again changing flags: 0x%08x --> 0x%08x", sfs->f_flags,
|
||||
newMode);
|
||||
|
||||
[self remount:disk mountMode:newMode semaphore:self.diskSema];
|
||||
|
||||
if (dispatch_semaphore_wait(self.diskSema,
|
||||
dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
|
||||
LOGW(@"Failed to remount device after unmounting: %s", sfs->f_mntfromname);
|
||||
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationRemountFailed];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationSuccess];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)logDiskAppeared:(NSDictionary *)props {
|
||||
self->_logger->LogDiskAppeared(props);
|
||||
}
|
||||
@@ -199,11 +419,11 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
}
|
||||
|
||||
- (void)enable {
|
||||
DARegisterDiskAppearedCallback(_diskArbSession, NULL, diskAppearedCallback,
|
||||
DARegisterDiskAppearedCallback(_diskArbSession, NULL, DiskAppearedCallback,
|
||||
(__bridge void *)self);
|
||||
DARegisterDiskDescriptionChangedCallback(_diskArbSession, NULL, NULL,
|
||||
diskDescriptionChangedCallback, (__bridge void *)self);
|
||||
DARegisterDiskDisappearedCallback(_diskArbSession, NULL, diskDisappearedCallback,
|
||||
DiskDescriptionChangedCallback, (__bridge void *)self);
|
||||
DARegisterDiskDisappearedCallback(_diskArbSession, NULL, DiskDisappearedCallback,
|
||||
(__bridge void *)self);
|
||||
|
||||
[super subscribeAndClearCache:{
|
||||
@@ -225,44 +445,15 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
long mountMode = eventStatFS->f_flags;
|
||||
pid_t pid = audit_token_to_pid(m->process->audit_token);
|
||||
LOGD(
|
||||
@"SNTEndpointSecurityDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %lu",
|
||||
m->process->executable->path.data, pid, mountMode);
|
||||
@"SNTEndpointSecurityDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %u",
|
||||
m->process->executable->path.data, pid, eventStatFS->f_flags);
|
||||
|
||||
DADiskRef disk = DADiskCreateFromBSDName(NULL, self.diskArbSession, eventStatFS->f_mntfromname);
|
||||
CFAutorelease(disk);
|
||||
|
||||
// TODO(tnek): Log all of the other attributes available in diskInfo into a structured log format.
|
||||
NSDictionary *diskInfo = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
BOOL isInternal = [diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceInternalKey] boolValue];
|
||||
BOOL isRemovable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaRemovableKey] boolValue];
|
||||
BOOL isEjectable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaEjectableKey] boolValue];
|
||||
NSString *protocol = diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey];
|
||||
BOOL isUSB = [protocol isEqualToString:@"USB"];
|
||||
BOOL isSecureDigital = [protocol isEqualToString:@"Secure Digital"];
|
||||
BOOL isVirtual = [protocol isEqualToString:@"Virtual Interface"];
|
||||
|
||||
NSString *kind = diskInfo[(__bridge NSString *)kDADiskDescriptionMediaKindKey];
|
||||
|
||||
// TODO: check kind and protocol for banned things (e.g. MTP).
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d "
|
||||
@"isRemovable: %d "
|
||||
@"isEjectable: %d",
|
||||
protocol, kind, isInternal, isRemovable, isEjectable);
|
||||
|
||||
// if the device is internal, or virtual *AND* is not an SD Card,
|
||||
// then allow the mount. This is to ensure we block SD cards inserted into
|
||||
// the internal reader of some Macs, whilst also ensuring we don't block
|
||||
// the internal storage device.
|
||||
if ((isInternal || isVirtual) && !isSecureDigital) {
|
||||
return ES_AUTH_RESULT_ALLOW;
|
||||
}
|
||||
|
||||
// We are okay with operations for devices that are non-removable as long as
|
||||
// they are NOT a USB device, or an SD Card.
|
||||
if (!isRemovable && !isEjectable && !isUSB && !isSecureDigital) {
|
||||
if (![self shouldOperateOnDisk:disk]) {
|
||||
return ES_AUTH_RESULT_ALLOW;
|
||||
}
|
||||
|
||||
@@ -270,24 +461,20 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
initWithOnName:[NSString stringWithUTF8String:eventStatFS->f_mntonname]
|
||||
fromName:[NSString stringWithUTF8String:eventStatFS->f_mntfromname]];
|
||||
|
||||
BOOL shouldRemount = self.remountArgs != nil && [self.remountArgs count] > 0;
|
||||
|
||||
if (shouldRemount) {
|
||||
if ([self haveRemountArgs]) {
|
||||
event.remountArgs = self.remountArgs;
|
||||
long remountOpts = mountArgsToMask(self.remountArgs);
|
||||
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: mountMode: %@", maskToMountArgs(mountMode));
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: remountOpts: %@", maskToMountArgs(remountOpts));
|
||||
|
||||
if ((mountMode & remountOpts) == remountOpts && m->event_type != ES_EVENT_TYPE_AUTH_REMOUNT) {
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: Allowing as mount as flags match remountOpts");
|
||||
if ([self remountUSBModeContainsFlags:eventStatFS->f_flags] &&
|
||||
m->event_type != ES_EVENT_TYPE_AUTH_REMOUNT) {
|
||||
LOGD(@"Allowing mount as flags contain RemountUSBMode. '%s' -> '%s'",
|
||||
eventStatFS->f_mntfromname, eventStatFS->f_mntonname);
|
||||
return ES_AUTH_RESULT_ALLOW;
|
||||
}
|
||||
|
||||
long newMode = mountMode | remountOpts;
|
||||
LOGI(@"SNTEndpointSecurityDeviceManager: remounting device '%s'->'%s', flags (%lu) -> (%lu)",
|
||||
eventStatFS->f_mntfromname, eventStatFS->f_mntonname, mountMode, newMode);
|
||||
[self remount:disk mountMode:newMode];
|
||||
uint32_t newMode = [self updatedMountFlags:eventStatFS];
|
||||
LOGI(@"SNTEndpointSecurityDeviceManager: remounting device '%s'->'%s', flags (%u) -> (%u)",
|
||||
eventStatFS->f_mntfromname, eventStatFS->f_mntonname, eventStatFS->f_flags, newMode);
|
||||
[self remount:disk mountMode:newMode semaphore:nil];
|
||||
}
|
||||
|
||||
if (self.deviceBlockCallback) {
|
||||
@@ -297,14 +484,16 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return ES_AUTH_RESULT_DENY;
|
||||
}
|
||||
|
||||
- (void)remount:(DADiskRef)disk mountMode:(long)remountMask {
|
||||
- (void)remount:(DADiskRef)disk
|
||||
mountMode:(uint32_t)remountMask
|
||||
semaphore:(nullable dispatch_semaphore_t)sema {
|
||||
NSArray<NSString *> *args = maskToMountArgs(remountMask);
|
||||
CFStringRef *argv = (CFStringRef *)calloc(args.count + 1, sizeof(CFStringRef));
|
||||
CFArrayGetValues((__bridge CFArrayRef)args, CFRangeMake(0, (CFIndex)args.count),
|
||||
(const void **)argv);
|
||||
|
||||
DADiskMountWithArguments(disk, NULL, kDADiskMountOptionDefault, diskMountedCallback,
|
||||
(__bridge void *)self, (CFStringRef *)argv);
|
||||
DADiskMountWithArguments(disk, NULL, kDADiskMountOptionDefault, DiskMountedCallback,
|
||||
(__bridge void *)sema, (CFStringRef *)argv);
|
||||
|
||||
free(argv);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
@@ -50,12 +51,17 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManager (Testing)
|
||||
- (instancetype)init;
|
||||
- (void)logDiskAppeared:(NSDictionary *)props;
|
||||
- (BOOL)shouldOperateOnDisk:(DADiskRef)disk;
|
||||
- (void)performStartupTasks:(SNTDeviceManagerStartupPreferences)startupPrefs;
|
||||
- (uint32_t)updatedMountFlags:(struct statfs *)sfs;
|
||||
@end
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManagerTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@property MockDiskArbitration *mockDA;
|
||||
@property MockMounts *mockMounts;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityDeviceManagerTest
|
||||
@@ -70,6 +76,9 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
self.mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[self.mockDA reset];
|
||||
|
||||
self.mockMounts = [MockMounts mockMounts];
|
||||
[self.mockMounts reset];
|
||||
|
||||
fclose(stdout);
|
||||
}
|
||||
|
||||
@@ -112,7 +121,10 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
logger:nullptr
|
||||
authResultCache:nullptr];
|
||||
authResultCache:nullptr
|
||||
blockUSBMount:false
|
||||
remountUSBMode:nil
|
||||
startupPreferences:SNTDeviceManagerStartupPreferencesNone];
|
||||
|
||||
setupDMCallback(deviceManager);
|
||||
|
||||
@@ -120,7 +132,7 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
id partialDeviceManager = OCMPartialMock(deviceManager);
|
||||
OCMStub([partialDeviceManager logDiskAppeared:OCMOCK_ANY]);
|
||||
|
||||
[self.mockDA insert:disk bsdName:test_mntfromname];
|
||||
[self.mockDA insert:disk];
|
||||
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
@@ -211,7 +223,8 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
};
|
||||
}];
|
||||
|
||||
XCTAssertEqual(self.mockDA.wasRemounted, YES);
|
||||
XCTAssertEqual(self.mockDA.insertedDevices.count, 1);
|
||||
XCTAssertTrue([self.mockDA.insertedDevices allValues][0].wasMounted);
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
@@ -274,7 +287,8 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
};
|
||||
}];
|
||||
|
||||
XCTAssertEqual(self.mockDA.wasRemounted, YES);
|
||||
XCTAssertEqual(self.mockDA.insertedDevices.count, 1);
|
||||
XCTAssertTrue([self.mockDA.insertedDevices allValues][0].wasMounted);
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:10.0];
|
||||
|
||||
@@ -303,7 +317,8 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
};
|
||||
}];
|
||||
|
||||
XCTAssertEqual(self.mockDA.wasRemounted, NO);
|
||||
XCTAssertEqual(self.mockDA.insertedDevices.count, 1);
|
||||
XCTAssertFalse([self.mockDA.insertedDevices allValues][0].wasMounted);
|
||||
}
|
||||
|
||||
- (void)testNotifyUnmountFlushesCache {
|
||||
@@ -324,7 +339,10 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
logger:nullptr
|
||||
authResultCache:mockAuthCache];
|
||||
authResultCache:mockAuthCache
|
||||
blockUSBMount:YES
|
||||
remountUSBMode:nil
|
||||
startupPreferences:SNTDeviceManagerStartupPreferencesNone];
|
||||
|
||||
deviceManager.blockUSBMount = YES;
|
||||
|
||||
@@ -340,6 +358,122 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get());
|
||||
}
|
||||
|
||||
- (void)testPerformStartupTasks {
|
||||
SNTEndpointSecurityDeviceManager *deviceManager = [[SNTEndpointSecurityDeviceManager alloc] init];
|
||||
|
||||
id partialDeviceManager = OCMPartialMock(deviceManager);
|
||||
OCMStub([partialDeviceManager shouldOperateOnDisk:nil]).ignoringNonObjectArgs().andReturn(YES);
|
||||
|
||||
deviceManager.blockUSBMount = YES;
|
||||
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];
|
||||
|
||||
[self.mockMounts insert:[[MockStatfs alloc] initFrom:@"d1" on:@"v1" flags:@(0x0)]];
|
||||
[self.mockMounts insert:[[MockStatfs alloc] initFrom:@"d2"
|
||||
on:@"v2"
|
||||
flags:@(MNT_RDONLY | MNT_NOEXEC | MNT_JOURNALED)]];
|
||||
|
||||
// Create mock disks with desired args
|
||||
MockDADisk * (^CreateMockDisk)(NSString *, NSString *) =
|
||||
^MockDADisk *(NSString *mountOn, NSString *mountFrom) {
|
||||
MockDADisk *mockDisk = [[MockDADisk alloc] init];
|
||||
mockDisk.diskDescription = @{
|
||||
@"DAVolumePath" : mountOn, // f_mntonname,
|
||||
@"DADevicePath" : mountOn, // f_mntonname,
|
||||
@"DAMediaBSDName" : mountFrom, // f_mntfromname,
|
||||
};
|
||||
|
||||
return mockDisk;
|
||||
};
|
||||
|
||||
// Reset the Mock DA property, setup disks and remount args, then trigger the test
|
||||
void (^PerformStartupTest)(NSArray<MockDADisk *> *, NSArray<NSString *> *,
|
||||
SNTDeviceManagerStartupPreferences) =
|
||||
^void(NSArray<MockDADisk *> *disks, NSArray<NSString *> *remountArgs,
|
||||
SNTDeviceManagerStartupPreferences startupPref) {
|
||||
[self.mockDA reset];
|
||||
|
||||
for (MockDADisk *d in disks) {
|
||||
[self.mockDA insert:d];
|
||||
}
|
||||
|
||||
deviceManager.remountArgs = remountArgs;
|
||||
|
||||
[deviceManager performStartupTasks:startupPref];
|
||||
};
|
||||
|
||||
// Unmount with RemountUSBMode set
|
||||
{
|
||||
MockDADisk *disk1 = CreateMockDisk(@"v1", @"d1");
|
||||
MockDADisk *disk2 = CreateMockDisk(@"v2", @"d2");
|
||||
|
||||
PerformStartupTest(@[ disk1, disk2 ], @[ @"noexec", @"rdonly" ],
|
||||
SNTDeviceManagerStartupPreferencesUnmount);
|
||||
|
||||
XCTAssertTrue(disk1.wasUnmounted);
|
||||
XCTAssertFalse(disk1.wasMounted);
|
||||
XCTAssertFalse(disk2.wasUnmounted);
|
||||
XCTAssertFalse(disk2.wasMounted);
|
||||
}
|
||||
|
||||
// Unmount with RemountUSBMode nil
|
||||
{
|
||||
MockDADisk *disk1 = CreateMockDisk(@"v1", @"d1");
|
||||
MockDADisk *disk2 = CreateMockDisk(@"v2", @"d2");
|
||||
|
||||
PerformStartupTest(@[ disk1, disk2 ], nil, SNTDeviceManagerStartupPreferencesUnmount);
|
||||
|
||||
XCTAssertTrue(disk1.wasUnmounted);
|
||||
XCTAssertFalse(disk1.wasMounted);
|
||||
XCTAssertTrue(disk2.wasUnmounted);
|
||||
XCTAssertFalse(disk2.wasMounted);
|
||||
}
|
||||
|
||||
// Remount with RemountUSBMode set
|
||||
{
|
||||
MockDADisk *disk1 = CreateMockDisk(@"v1", @"d1");
|
||||
MockDADisk *disk2 = CreateMockDisk(@"v2", @"d2");
|
||||
|
||||
PerformStartupTest(@[ disk1, disk2 ], @[ @"noexec", @"rdonly" ],
|
||||
SNTDeviceManagerStartupPreferencesRemount);
|
||||
|
||||
XCTAssertTrue(disk1.wasUnmounted);
|
||||
XCTAssertTrue(disk1.wasMounted);
|
||||
XCTAssertFalse(disk2.wasUnmounted);
|
||||
XCTAssertFalse(disk2.wasMounted);
|
||||
}
|
||||
|
||||
// Unmount with RemountUSBMode nil
|
||||
{
|
||||
MockDADisk *disk1 = CreateMockDisk(@"v1", @"d1");
|
||||
MockDADisk *disk2 = CreateMockDisk(@"v2", @"d2");
|
||||
|
||||
PerformStartupTest(@[ disk1, disk2 ], nil, SNTDeviceManagerStartupPreferencesRemount);
|
||||
|
||||
XCTAssertTrue(disk1.wasUnmounted);
|
||||
XCTAssertFalse(disk1.wasMounted);
|
||||
XCTAssertTrue(disk2.wasUnmounted);
|
||||
XCTAssertFalse(disk2.wasMounted);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testUpdatedMountFlags {
|
||||
struct statfs sfs;
|
||||
|
||||
strlcpy(sfs.f_fstypename, "foo", sizeof(sfs.f_fstypename));
|
||||
sfs.f_flags = MNT_JOURNALED | MNT_NOSUID | MNT_NODEV;
|
||||
|
||||
SNTEndpointSecurityDeviceManager *deviceManager = [[SNTEndpointSecurityDeviceManager alloc] init];
|
||||
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];
|
||||
|
||||
// For most filesystems, the flags are the union of what is in statfs and the remount args
|
||||
XCTAssertEqual([deviceManager updatedMountFlags:&sfs], sfs.f_flags | MNT_RDONLY | MNT_NOEXEC);
|
||||
|
||||
// For APFS, flags are still unioned, but MNT_JOUNRNALED is cleared
|
||||
strlcpy(sfs.f_fstypename, "apfs", sizeof(sfs.f_fstypename));
|
||||
XCTAssertEqual([deviceManager updatedMountFlags:&sfs],
|
||||
(sfs.f_flags | MNT_RDONLY | MNT_NOEXEC) & ~MNT_JOURNALED);
|
||||
}
|
||||
|
||||
- (void)testEnable {
|
||||
// Ensure the client subscribes to expected event types
|
||||
std::set<es_event_type_t> expectedEventSubs{
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTFileAccessEvent.h"
|
||||
#include "Source/santad/DataLayer/WatchItems.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
@@ -24,6 +25,10 @@
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
#include "Source/santad/TTYWriter.h"
|
||||
|
||||
typedef void (^SNTFileAccessBlockCallback)(SNTFileAccessEvent *event, NSString *customMsg,
|
||||
NSString *customURL, NSString *customText);
|
||||
|
||||
@interface SNTEndpointSecurityFileAccessAuthorizer
|
||||
: SNTEndpointSecurityClient <SNTEndpointSecurityDynamicEventHandler>
|
||||
@@ -36,6 +41,9 @@
|
||||
watchItems:(std::shared_ptr<santa::santad::data_layer::WatchItems>)watchItems
|
||||
enricher:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
|
||||
decisionCache:(SNTDecisionCache *)decisionCache;
|
||||
decisionCache:(SNTDecisionCache *)decisionCache
|
||||
ttyWriter:(std::shared_ptr<santa::santad::TTYWriter>)ttyWriter;
|
||||
|
||||
@property SNTFileAccessBlockCallback fileAccessBlockCallback;
|
||||
|
||||
@end
|
||||
|
||||
@@ -18,32 +18,45 @@
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <sys/fcntl.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
#include "Source/common/Platform.h"
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#include "Source/common/SNTFileAccessEvent.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#import "Source/common/SNTStrengthify.h"
|
||||
#include "Source/common/SantaCache.h"
|
||||
#include "Source/common/SantaVnode.h"
|
||||
#include "Source/common/SantaVnodeHash.h"
|
||||
#include "Source/common/String.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/DataLayer/WatchItems.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/RateLimiter.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
|
||||
using santa::common::OptionalStringToNSString;
|
||||
using santa::common::StringToNSString;
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::FileAccessMetricStatus;
|
||||
using santa::santad::Metrics;
|
||||
using santa::santad::TTYWriter;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
using santa::santad::data_layer::WatchItemPolicy;
|
||||
using santa::santad::data_layer::WatchItems;
|
||||
@@ -65,6 +78,122 @@ static constexpr uint16_t kDefaultRateLimitQPS = 50;
|
||||
struct PathTarget {
|
||||
std::string path;
|
||||
bool isReadable;
|
||||
std::optional<std::pair<dev_t, ino_t>> devnoIno;
|
||||
};
|
||||
|
||||
// This is a bespoke cache for mapping processes to a set of values. It has
|
||||
// similar semantics to SantaCache in terms of clearing the cache keys and
|
||||
// values when max sizes are reached.
|
||||
//
|
||||
// TODO: We need a proper LRU cache
|
||||
//
|
||||
// NB: This exists instead of using SantaCache for two main reasons:
|
||||
// 1.) SantaCache doesn't efficiently support non-primitive value types.
|
||||
// Since the value of each key needs to be a set, we want to refrain
|
||||
// from having to unnecessarily copy the value.
|
||||
// 2.) SantaCache doesn't support size limits on value types
|
||||
template <typename ValueT>
|
||||
class ProcessSet {
|
||||
using FileSet = absl::flat_hash_set<ValueT>;
|
||||
|
||||
public:
|
||||
ProcessSet() {
|
||||
q_ = dispatch_queue_create(
|
||||
"com.google.santa.daemon.faa",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
|
||||
QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
};
|
||||
|
||||
// Add the given target to the set of files a process can read
|
||||
void Set(const es_process_t *proc, std::function<ValueT()> valueBlock) {
|
||||
if (!valueBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_sync(q_, ^{
|
||||
std::pair<pid_t, pid_t> pidPidver = {audit_token_to_pid(proc->audit_token),
|
||||
audit_token_to_pidversion(proc->audit_token)};
|
||||
SetLocked(pidPidver, valueBlock());
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the given process from the cache
|
||||
void Remove(const es_process_t *proc) {
|
||||
std::pair<pid_t, pid_t> pidPidver = {audit_token_to_pid(proc->audit_token),
|
||||
audit_token_to_pidversion(proc->audit_token)};
|
||||
dispatch_sync(q_, ^{
|
||||
cache_.erase(pidPidver);
|
||||
});
|
||||
}
|
||||
|
||||
// Check if the set of files for a given process contains the given file
|
||||
bool Exists(const es_process_t *proc, std::function<ValueT()> valueBlock) {
|
||||
return ExistsOrSet(proc, valueBlock, false);
|
||||
}
|
||||
|
||||
// Check if the ValueT set for a given process contains the given file, and
|
||||
// if not, set it. Both steps are done atomically.
|
||||
bool ExistsOrSet(const es_process_t *proc, std::function<ValueT()> valueBlock) {
|
||||
return ExistsOrSet(proc, valueBlock, true);
|
||||
}
|
||||
|
||||
// Clear all cache entries
|
||||
void Clear() {
|
||||
dispatch_sync(q_, ^{
|
||||
ClearLocked();
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
// Remove everything in the cache.
|
||||
void ClearLocked() { cache_.clear(); }
|
||||
|
||||
void SetLocked(std::pair<pid_t, pid_t> pidPidver, ValueT value) {
|
||||
// If we hit the size limit, clear the cache to prevent unbounded growth
|
||||
if (cache_.size() >= kMaxCacheSize) {
|
||||
ClearLocked();
|
||||
}
|
||||
|
||||
FileSet &fs = cache_[std::move(pidPidver)];
|
||||
|
||||
// If we hit the per-entry size limit, clear the entry to prevent unbounded growth
|
||||
if (fs.size() >= kMaxCacheEntrySize) {
|
||||
fs.clear();
|
||||
}
|
||||
|
||||
fs.insert(value);
|
||||
}
|
||||
|
||||
bool ExistsOrSet(const es_process_t *proc, std::function<ValueT()> valueBlock, bool shouldSet) {
|
||||
std::pair<pid_t, pid_t> pidPidver = {audit_token_to_pid(proc->audit_token),
|
||||
audit_token_to_pidversion(proc->audit_token)};
|
||||
|
||||
__block bool exists = false;
|
||||
|
||||
dispatch_sync(q_, ^{
|
||||
ValueT value = valueBlock();
|
||||
const auto &iter = cache_.find(pidPidver);
|
||||
|
||||
if (iter != cache_.end() && iter->second.count(value) > 0) {
|
||||
exists = true;
|
||||
} else if (shouldSet) {
|
||||
SetLocked(pidPidver, value);
|
||||
}
|
||||
});
|
||||
|
||||
return exists;
|
||||
}
|
||||
|
||||
dispatch_queue_t q_;
|
||||
absl::flat_hash_map<std::pair<pid_t, pid_t>, FileSet> cache_;
|
||||
|
||||
// Cache limits are merely meant to protect against unbounded growth. In practice,
|
||||
// the observed cache size is typically small for normal WatchItems rules (those
|
||||
// that do not target high-volume paths). The per entry size was observed to vary
|
||||
// quite dramatically based on the type of process (e.g. large, complex applications
|
||||
// were observed to frequently have several thousands of entries).
|
||||
static constexpr size_t kMaxCacheSize = 512;
|
||||
static constexpr size_t kMaxCacheEntrySize = 8192;
|
||||
};
|
||||
|
||||
static inline std::string Path(const es_file_t *esFile) {
|
||||
@@ -78,14 +207,18 @@ static inline std::string Path(const es_string_token_t &tok) {
|
||||
static inline void PushBackIfNotTruncated(std::vector<PathTarget> &vec, const es_file_t *esFile,
|
||||
bool isReadable = false) {
|
||||
if (!esFile->path_truncated) {
|
||||
vec.push_back({Path(esFile), isReadable});
|
||||
vec.push_back({Path(esFile), isReadable,
|
||||
isReadable ? std::make_optional<std::pair<dev_t, ino_t>>(
|
||||
{esFile->stat.st_dev, esFile->stat.st_ino})
|
||||
: std::nullopt});
|
||||
}
|
||||
}
|
||||
|
||||
// Note: This variant of PushBackIfNotTruncated can never be marked "isReadable"
|
||||
static inline void PushBackIfNotTruncated(std::vector<PathTarget> &vec, const es_file_t *dir,
|
||||
const es_string_token_t &name, bool isReadable = false) {
|
||||
const es_string_token_t &name) {
|
||||
if (!dir->path_truncated) {
|
||||
vec.push_back({Path(dir) + "/" + Path(name), isReadable});
|
||||
vec.push_back({Path(dir) + "/" + Path(name), false, std::nullopt});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,9 +232,43 @@ es_auth_result_t FileAccessPolicyDecisionToESAuthResult(FileAccessPolicyDecision
|
||||
case FileAccessPolicyDecision::kAllowedAuditOnly: return ES_AUTH_RESULT_ALLOW;
|
||||
default:
|
||||
// This is a programming error. Bail.
|
||||
LOGE(@"Invalid file access decision encountered: %d", decision);
|
||||
LOGE(@"Invalid file access decision encountered: %d", static_cast<int>(decision));
|
||||
[NSException raise:@"Invalid FileAccessPolicyDecision"
|
||||
format:@"Invalid FileAccessPolicyDecision: %d", decision];
|
||||
format:@"Invalid FileAccessPolicyDecision: %d", static_cast<int>(decision)];
|
||||
}
|
||||
}
|
||||
|
||||
bool IsBlockDecision(FileAccessPolicyDecision decision) {
|
||||
return decision == FileAccessPolicyDecision::kDenied ||
|
||||
decision == FileAccessPolicyDecision::kDeniedInvalidSignature;
|
||||
}
|
||||
|
||||
FileAccessPolicyDecision ApplyOverrideToDecision(FileAccessPolicyDecision decision,
|
||||
SNTOverrideFileAccessAction overrideAction) {
|
||||
switch (overrideAction) {
|
||||
// When no override should be applied, return the decision unmodified
|
||||
case SNTOverrideFileAccessActionNone: return decision;
|
||||
|
||||
// When the decision should be overridden to be audit only, only change the
|
||||
// decision if it was going to deny the operation.
|
||||
case SNTOverrideFileAccessActionAuditOnly:
|
||||
if (IsBlockDecision(decision)) {
|
||||
return FileAccessPolicyDecision::kAllowedAuditOnly;
|
||||
} else {
|
||||
return decision;
|
||||
}
|
||||
|
||||
// If the override action is to disable policy, return a decision that will
|
||||
// be treated as if no policy applied to the operation.
|
||||
case SNTOverrideFileAccessActionDiable: return FileAccessPolicyDecision::kNoPolicy;
|
||||
|
||||
default:
|
||||
// This is a programming error. Bail.
|
||||
LOGE(@"Invalid override file access action encountered: %d",
|
||||
static_cast<int>(overrideAction));
|
||||
[NSException
|
||||
raise:@"Invalid SNTOverrideFileAccessAction"
|
||||
format:@"Invalid SNTOverrideFileAccessAction: %d", static_cast<int>(overrideAction)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +281,10 @@ bool ShouldLogDecision(FileAccessPolicyDecision decision) {
|
||||
}
|
||||
}
|
||||
|
||||
bool ShouldNotifyUserDecision(FileAccessPolicyDecision decision) {
|
||||
return ShouldLogDecision(decision) && decision != FileAccessPolicyDecision::kAllowedAuditOnly;
|
||||
}
|
||||
|
||||
es_auth_result_t CombinePolicyResults(es_auth_result_t result1, es_auth_result_t result2) {
|
||||
// If either policy denied the operation, the operation is denied
|
||||
return ((result1 == ES_AUTH_RESULT_DENY || result2 == ES_AUTH_RESULT_DENY)
|
||||
@@ -192,7 +363,23 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
}
|
||||
}
|
||||
|
||||
bool ShouldMessageTTY(const std::shared_ptr<WatchItemPolicy> &policy, const Message &msg,
|
||||
ProcessSet<std::pair<std::string, std::string>> &ttyMessageCache) {
|
||||
if (policy->silent_tty || !TTYWriter::CanWrite(msg->process)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ExistsOrSet returns `true` if the item existed. However we want to invert
|
||||
// this result as the return value for this function since we want to message
|
||||
// the TTY only when `ExistsOrSet` was a "set" operation, meaning it was the
|
||||
// first time this value was added.
|
||||
return !ttyMessageCache.ExistsOrSet(msg->process, ^std::pair<std::string, std::string>() {
|
||||
return {policy->version, policy->name};
|
||||
});
|
||||
}
|
||||
|
||||
@interface SNTEndpointSecurityFileAccessAuthorizer ()
|
||||
@property SNTConfigurator *configurator;
|
||||
@property SNTDecisionCache *decisionCache;
|
||||
@property bool isSubscribed;
|
||||
@end
|
||||
@@ -203,17 +390,22 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
std::shared_ptr<Enricher> _enricher;
|
||||
std::shared_ptr<RateLimiter> _rateLimiter;
|
||||
SantaCache<SantaVnode, NSString *> _certHashCache;
|
||||
std::shared_ptr<TTYWriter> _ttyWriter;
|
||||
ProcessSet<std::pair<dev_t, ino_t>> _readsCache;
|
||||
ProcessSet<std::pair<std::string, std::string>> _ttyMessageCache;
|
||||
std::shared_ptr<Metrics> _metrics;
|
||||
}
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
metrics:(std::shared_ptr<Metrics>)metrics
|
||||
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
|
||||
watchItems:(std::shared_ptr<WatchItems>)watchItems
|
||||
enricher:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
|
||||
decisionCache:(SNTDecisionCache *)decisionCache {
|
||||
decisionCache:(SNTDecisionCache *)decisionCache
|
||||
ttyWriter:(std::shared_ptr<santa::santad::TTYWriter>)ttyWriter {
|
||||
self = [super initWithESAPI:std::move(esApi)
|
||||
metrics:metrics
|
||||
processor:santa::santad::Processor::kFileAccessAuthorizer];
|
||||
@@ -221,10 +413,13 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
_watchItems = std::move(watchItems);
|
||||
_logger = std::move(logger);
|
||||
_enricher = std::move(enricher);
|
||||
|
||||
_decisionCache = decisionCache;
|
||||
_ttyWriter = std::move(ttyWriter);
|
||||
_metrics = std::move(metrics);
|
||||
|
||||
_rateLimiter = RateLimiter::Create(metrics, santa::santad::Processor::kFileAccessAuthorizer,
|
||||
_configurator = [SNTConfigurator configurator];
|
||||
|
||||
_rateLimiter = RateLimiter::Create(_metrics, santa::santad::Processor::kFileAccessAuthorizer,
|
||||
kDefaultRateLimitQPS);
|
||||
|
||||
SNTMetricBooleanGauge *famEnabled = [[SNTMetricSet sharedInstance]
|
||||
@@ -407,7 +602,7 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
|
||||
// If the process is signed but has an invalid signature, it is denied
|
||||
if (((msg->process->codesigning_flags & (CS_SIGNED | CS_VALID)) == CS_SIGNED) &&
|
||||
[[SNTConfigurator configurator] enableBadSignatureProtection]) {
|
||||
[self.configurator enableBadSignatureProtection]) {
|
||||
// TODO(mlw): Think about how to make stronger guarantees here to handle
|
||||
// programs becoming invalid after first being granted access. Maybe we
|
||||
// should only allow things that have hardened runtime flags set?
|
||||
@@ -416,6 +611,13 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
|
||||
std::shared_ptr<WatchItemPolicy> policy = optionalPolicy.value();
|
||||
|
||||
// If policy allows reading, add target to the cache
|
||||
if (policy->allow_read_access && target.devnoIno.has_value()) {
|
||||
self->_readsCache.Set(msg->process, ^{
|
||||
return *target.devnoIno;
|
||||
});
|
||||
}
|
||||
|
||||
// Check if this action contains any special case that would produce
|
||||
// an immediate result.
|
||||
FileAccessPolicyDecision specialCase = [self specialCaseForPolicy:policy
|
||||
@@ -449,10 +651,6 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
decision = FileAccessPolicyDecision::kAllowedAuditOnly;
|
||||
}
|
||||
|
||||
// https://github.com/google/santa/issues/1084
|
||||
// TODO(xyz): Write to TTY like in exec controller?
|
||||
// TODO(xyz): Need new config item for custom message in UI
|
||||
|
||||
return decision;
|
||||
}
|
||||
|
||||
@@ -460,16 +658,25 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
target:(const PathTarget &)target
|
||||
policy:
|
||||
(std::optional<std::shared_ptr<WatchItemPolicy>>)optionalPolicy
|
||||
policyVersion:(const std::string &)policyVersion {
|
||||
FileAccessPolicyDecision policyDecision = [self applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:msg];
|
||||
policyVersion:(const std::string &)policyVersion
|
||||
overrideAction:(SNTOverrideFileAccessAction)overrideAction {
|
||||
FileAccessPolicyDecision policyDecision = ApplyOverrideToDecision(
|
||||
[self applyPolicy:optionalPolicy forTarget:target toMessage:msg], overrideAction);
|
||||
|
||||
// Note: If ShouldLogDecision, it shouldn't be possible for optionalPolicy
|
||||
// to not have a value. Performing the check just in case to prevent a crash.
|
||||
if (ShouldLogDecision(policyDecision) && optionalPolicy.has_value()) {
|
||||
if (_rateLimiter->Decide(msg->mach_time) == RateLimiter::Decision::kAllowed) {
|
||||
std::string policyNameCopy = optionalPolicy.value()->name;
|
||||
std::shared_ptr<WatchItemPolicy> policy = optionalPolicy.value();
|
||||
RateLimiter::Decision decision = _rateLimiter->Decide(msg->mach_time);
|
||||
|
||||
self->_metrics->SetFileAccessEventMetrics(policyVersion, policy->name,
|
||||
(decision == RateLimiter::Decision::kAllowed)
|
||||
? FileAccessMetricStatus::kOK
|
||||
: FileAccessMetricStatus::kBlockedUser,
|
||||
msg->event_type, policyDecision);
|
||||
|
||||
if (decision == RateLimiter::Decision::kAllowed) {
|
||||
std::string policyNameCopy = policy->name;
|
||||
std::string policyVersionCopy = policyVersion;
|
||||
std::string targetPathCopy = target.path;
|
||||
|
||||
@@ -481,12 +688,70 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
targetPathCopy, policyDecision);
|
||||
}];
|
||||
}
|
||||
|
||||
// Notify users on block decisions
|
||||
if (ShouldNotifyUserDecision(policyDecision) &&
|
||||
(!policy->silent || (!policy->silent_tty && msg->process->tty->path.length > 0))) {
|
||||
SNTCachedDecision *cd =
|
||||
[self.decisionCache cachedDecisionForFile:msg->process->executable->stat];
|
||||
|
||||
SNTFileAccessEvent *event = [[SNTFileAccessEvent alloc] init];
|
||||
|
||||
event.accessedPath = StringToNSString(target.path);
|
||||
event.ruleVersion = StringToNSString(policy->version);
|
||||
event.ruleName = StringToNSString(policy->name);
|
||||
|
||||
event.fileSHA256 = cd.sha256 ?: @"<unknown sha>";
|
||||
event.filePath = StringToNSString(msg->process->executable->path.data);
|
||||
event.teamID = cd.teamID ?: @"<unknown team id>";
|
||||
event.teamID = cd.signingID ?: @"<unknown signing id>";
|
||||
event.pid = @(audit_token_to_pid(msg->process->audit_token));
|
||||
event.ppid = @(audit_token_to_pid(msg->process->parent_audit_token));
|
||||
event.parentName = StringToNSString(msg.ParentProcessName());
|
||||
event.signingChain = cd.certChain;
|
||||
|
||||
std::pair<NSString *, NSString *> linkInfo = self->_watchItems->EventDetailLinkInfo(policy);
|
||||
|
||||
if (!policy->silent && self.fileAccessBlockCallback) {
|
||||
self.fileAccessBlockCallback(event, OptionalStringToNSString(policy->custom_message),
|
||||
linkInfo.first, linkInfo.second);
|
||||
}
|
||||
|
||||
if (ShouldMessageTTY(policy, msg, self->_ttyMessageCache)) {
|
||||
NSAttributedString *attrStr =
|
||||
[SNTBlockMessage attributedBlockMessageForFileAccessEvent:event
|
||||
customMessage:OptionalStringToNSString(
|
||||
policy->custom_message)];
|
||||
|
||||
NSMutableString *blockMsg = [NSMutableString stringWithCapacity:1024];
|
||||
// Escape sequences `\033[1m` and `\033[0m` begin/end bold lettering
|
||||
[blockMsg appendFormat:@"\n\033[1mSanta\033[0m\n\n%@\n\n", attrStr.string];
|
||||
[blockMsg appendFormat:@"\033[1mAccessed Path:\033[0m %@\n"
|
||||
@"\033[1mRule Version: \033[0m %@\n"
|
||||
@"\033[1mRule Name: \033[0m %@\n"
|
||||
@"\n"
|
||||
@"\033[1mProcess Path: \033[0m %@\n"
|
||||
@"\033[1mIdentifier: \033[0m %@\n"
|
||||
@"\033[1mParent: \033[0m %@\n\n",
|
||||
event.accessedPath, event.ruleVersion, event.ruleName,
|
||||
event.filePath, event.fileSHA256, event.parentName];
|
||||
|
||||
NSURL *detailURL = [SNTBlockMessage eventDetailURLForFileAccessEvent:event
|
||||
customURL:linkInfo.first];
|
||||
if (detailURL) {
|
||||
[blockMsg appendFormat:@"More info:\n%@\n\n", detailURL.absoluteString];
|
||||
}
|
||||
|
||||
self->_ttyWriter->Write(msg->process, blockMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return policyDecision;
|
||||
}
|
||||
|
||||
- (void)processMessage:(const Message &)msg {
|
||||
- (void)processMessage:(const Message &)msg
|
||||
overrideAction:(SNTOverrideFileAccessAction)overrideAction {
|
||||
std::vector<PathTarget> targets;
|
||||
targets.reserve(2);
|
||||
PopulatePathTargets(msg, targets);
|
||||
@@ -508,7 +773,8 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
FileAccessPolicyDecision curDecision = [self handleMessage:msg
|
||||
target:targets[i]
|
||||
policy:versionAndPolicies.second[i]
|
||||
policyVersion:versionAndPolicies.first];
|
||||
policyVersion:versionAndPolicies.first
|
||||
overrideAction:overrideAction];
|
||||
|
||||
policyResult =
|
||||
CombinePolicyResults(policyResult, FileAccessPolicyDecisionToESAuthResult(curDecision));
|
||||
@@ -533,19 +799,44 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
|
||||
- (void)handleMessage:(santa::santad::event_providers::endpoint_security::Message &&)esMsg
|
||||
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
|
||||
SNTOverrideFileAccessAction overrideAction = [self.configurator overrideFileAccessAction];
|
||||
|
||||
// If the override action is set to Disable, return immediately.
|
||||
if (overrideAction == SNTOverrideFileAccessActionDiable) {
|
||||
if (esMsg->action_type == ES_ACTION_TYPE_AUTH) {
|
||||
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:false];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (esMsg->event_type == ES_EVENT_TYPE_AUTH_OPEN &&
|
||||
!(esMsg->event.open.fflag & kOpenFlagsIndicatingWrite)) {
|
||||
if (self->_readsCache.Exists(esMsg->process, ^std::pair<pid_t, pid_t> {
|
||||
return std::pair<dev_t, ino_t>{esMsg->event.open.file->stat.st_dev,
|
||||
esMsg->event.open.file->stat.st_ino};
|
||||
})) {
|
||||
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:false];
|
||||
return;
|
||||
}
|
||||
} else if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_EXIT) {
|
||||
// On process exit, remove the cache entry
|
||||
self->_readsCache.Remove(esMsg->process);
|
||||
self->_ttyMessageCache.Remove(esMsg->process);
|
||||
return;
|
||||
}
|
||||
|
||||
[self processMessage:std::move(esMsg)
|
||||
handler:^(const Message &msg) {
|
||||
[self processMessage:msg];
|
||||
[self processMessage:msg overrideAction:overrideAction];
|
||||
recordEventMetrics(EventDisposition::kProcessed);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)enable {
|
||||
// TODO(xyz): Expand to support ES_EVENT_TYPE_AUTH_CREATE, ES_EVENT_TYPE_AUTH_TRUNCATE
|
||||
std::set<es_event_type_t> events = {
|
||||
ES_EVENT_TYPE_AUTH_CLONE, ES_EVENT_TYPE_AUTH_CREATE, ES_EVENT_TYPE_AUTH_EXCHANGEDATA,
|
||||
ES_EVENT_TYPE_AUTH_LINK, ES_EVENT_TYPE_AUTH_OPEN, ES_EVENT_TYPE_AUTH_RENAME,
|
||||
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK,
|
||||
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK, ES_EVENT_TYPE_NOTIFY_EXIT,
|
||||
};
|
||||
|
||||
#if HAVE_MACOS_12
|
||||
@@ -589,6 +880,8 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
// begin receiving events (if not already)
|
||||
[self enable];
|
||||
}
|
||||
|
||||
self->_readsCache.Clear();
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/fcntl.h>
|
||||
#include <sys/types.h>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
@@ -31,6 +33,7 @@
|
||||
|
||||
#include "Source/common/Platform.h"
|
||||
#include "Source/common/SNTCachedDecision.h"
|
||||
#include "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
@@ -50,13 +53,22 @@ extern NSString *kBadCertHash;
|
||||
struct PathTarget {
|
||||
std::string path;
|
||||
bool isReadable;
|
||||
std::optional<std::pair<dev_t, ino_t>> devnoIno;
|
||||
};
|
||||
|
||||
using PathTargetsPair = std::pair<std::optional<std::string>, std::optional<std::string>>;
|
||||
extern void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets);
|
||||
extern es_auth_result_t FileAccessPolicyDecisionToESAuthResult(FileAccessPolicyDecision decision);
|
||||
extern bool ShouldLogDecision(FileAccessPolicyDecision decision);
|
||||
extern bool ShouldNotifyUserDecision(FileAccessPolicyDecision decision);
|
||||
extern es_auth_result_t CombinePolicyResults(es_auth_result_t result1, es_auth_result_t result2);
|
||||
extern bool IsBlockDecision(FileAccessPolicyDecision decision);
|
||||
extern FileAccessPolicyDecision ApplyOverrideToDecision(FileAccessPolicyDecision decision,
|
||||
SNTOverrideFileAccessAction overrideAction);
|
||||
|
||||
static inline std::pair<dev_t, ino_t> FileID(const es_file_t &file) {
|
||||
return std::make_pair(file.stat.st_dev, file.stat.st_ino);
|
||||
}
|
||||
|
||||
void SetExpectationsForFileAccessAuthorizerInit(
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi) {
|
||||
@@ -135,7 +147,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
logger:nullptr
|
||||
watchItems:nullptr
|
||||
enricher:nullptr
|
||||
decisionCache:self.dcMock];
|
||||
decisionCache:self.dcMock
|
||||
ttyWriter:nullptr];
|
||||
|
||||
//
|
||||
// Test 1 - Not in local cache or decision cache, and code sig lookup fails
|
||||
@@ -229,7 +242,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
{FileAccessPolicyDecision::kAllowed, false},
|
||||
{FileAccessPolicyDecision::kAllowedReadAccess, false},
|
||||
{FileAccessPolicyDecision::kAllowedAuditOnly, true},
|
||||
{(FileAccessPolicyDecision)5, false},
|
||||
{(FileAccessPolicyDecision)123, false},
|
||||
};
|
||||
|
||||
for (const auto &kv : policyDecisionToShouldLog) {
|
||||
@@ -237,6 +250,79 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testShouldNotifyUserDecision {
|
||||
std::map<FileAccessPolicyDecision, bool> policyDecisionToShouldLog = {
|
||||
{FileAccessPolicyDecision::kNoPolicy, false},
|
||||
{FileAccessPolicyDecision::kDenied, true},
|
||||
{FileAccessPolicyDecision::kDeniedInvalidSignature, true},
|
||||
{FileAccessPolicyDecision::kAllowed, false},
|
||||
{FileAccessPolicyDecision::kAllowedReadAccess, false},
|
||||
{FileAccessPolicyDecision::kAllowedAuditOnly, false},
|
||||
{(FileAccessPolicyDecision)123, false},
|
||||
};
|
||||
|
||||
for (const auto &kv : policyDecisionToShouldLog) {
|
||||
XCTAssertEqual(ShouldNotifyUserDecision(kv.first), kv.second);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testIsBlockDecision {
|
||||
std::map<FileAccessPolicyDecision, bool> policyDecisionToIsBlockDecision = {
|
||||
{FileAccessPolicyDecision::kNoPolicy, false},
|
||||
{FileAccessPolicyDecision::kDenied, true},
|
||||
{FileAccessPolicyDecision::kDeniedInvalidSignature, true},
|
||||
{FileAccessPolicyDecision::kAllowed, false},
|
||||
{FileAccessPolicyDecision::kAllowedReadAccess, false},
|
||||
{FileAccessPolicyDecision::kAllowedAuditOnly, false},
|
||||
{(FileAccessPolicyDecision)123, false},
|
||||
};
|
||||
|
||||
for (const auto &kv : policyDecisionToIsBlockDecision) {
|
||||
XCTAssertEqual(ShouldNotifyUserDecision(kv.first), kv.second);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testApplyOverrideToDecision {
|
||||
std::map<std::pair<FileAccessPolicyDecision, SNTOverrideFileAccessAction>,
|
||||
FileAccessPolicyDecision>
|
||||
decisionAndOverrideToDecision = {
|
||||
// Override action: None - Policy shouldn't be changed
|
||||
{{FileAccessPolicyDecision::kNoPolicy, SNTOverrideFileAccessActionNone},
|
||||
FileAccessPolicyDecision::kNoPolicy},
|
||||
{{FileAccessPolicyDecision::kDenied, SNTOverrideFileAccessActionNone},
|
||||
FileAccessPolicyDecision::kDenied},
|
||||
|
||||
// Override action: AuditOnly - Policy should be changed only on blocked decisions
|
||||
{{FileAccessPolicyDecision::kNoPolicy, SNTOverrideFileAccessActionAuditOnly},
|
||||
FileAccessPolicyDecision::kNoPolicy},
|
||||
{{FileAccessPolicyDecision::kAllowedAuditOnly, SNTOverrideFileAccessActionAuditOnly},
|
||||
FileAccessPolicyDecision::kAllowedAuditOnly},
|
||||
{{FileAccessPolicyDecision::kAllowedReadAccess, SNTOverrideFileAccessActionAuditOnly},
|
||||
FileAccessPolicyDecision::kAllowedReadAccess},
|
||||
{{FileAccessPolicyDecision::kDenied, SNTOverrideFileAccessActionAuditOnly},
|
||||
FileAccessPolicyDecision::kAllowedAuditOnly},
|
||||
{{FileAccessPolicyDecision::kDeniedInvalidSignature, SNTOverrideFileAccessActionAuditOnly},
|
||||
FileAccessPolicyDecision::kAllowedAuditOnly},
|
||||
|
||||
// Override action: Disable - Always changes the decision to be no policy applied
|
||||
{{FileAccessPolicyDecision::kAllowed, SNTOverrideFileAccessActionDiable},
|
||||
FileAccessPolicyDecision::kNoPolicy},
|
||||
{{FileAccessPolicyDecision::kDenied, SNTOverrideFileAccessActionDiable},
|
||||
FileAccessPolicyDecision::kNoPolicy},
|
||||
{{FileAccessPolicyDecision::kAllowedReadAccess, SNTOverrideFileAccessActionDiable},
|
||||
FileAccessPolicyDecision::kNoPolicy},
|
||||
{{FileAccessPolicyDecision::kAllowedAuditOnly, SNTOverrideFileAccessActionDiable},
|
||||
FileAccessPolicyDecision::kNoPolicy},
|
||||
};
|
||||
|
||||
for (const auto &kv : decisionAndOverrideToDecision) {
|
||||
XCTAssertEqual(ApplyOverrideToDecision(kv.first.first, kv.first.second), kv.second);
|
||||
}
|
||||
|
||||
XCTAssertThrows(
|
||||
ApplyOverrideToDecision(FileAccessPolicyDecision::kAllowed, (SNTOverrideFileAccessAction)123));
|
||||
}
|
||||
|
||||
- (void)testCombinePolicyResults {
|
||||
// Ensure that the combined result is ES_AUTH_RESULT_DENY if both or either
|
||||
// input result is ES_AUTH_RESULT_DENY.
|
||||
@@ -269,7 +355,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
logger:nullptr
|
||||
watchItems:nullptr
|
||||
enricher:nullptr
|
||||
decisionCache:nil];
|
||||
decisionCache:nil
|
||||
ttyWriter:nullptr];
|
||||
|
||||
auto policy = std::make_shared<WatchItemPolicy>("foo_policy", "/foo");
|
||||
|
||||
@@ -397,7 +484,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
logger:nullptr
|
||||
watchItems:nullptr
|
||||
enricher:nullptr
|
||||
decisionCache:nil];
|
||||
decisionCache:nil
|
||||
ttyWriter:nullptr];
|
||||
|
||||
id accessClientMock = OCMPartialMock(accessClient);
|
||||
|
||||
@@ -515,7 +603,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
logger:nullptr
|
||||
watchItems:nullptr
|
||||
enricher:nullptr
|
||||
decisionCache:nil];
|
||||
decisionCache:nil
|
||||
ttyWriter:nullptr];
|
||||
|
||||
id accessClientMock = OCMPartialMock(accessClient);
|
||||
|
||||
@@ -642,7 +731,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
std::set<es_event_type_t> expectedEventSubs = {
|
||||
ES_EVENT_TYPE_AUTH_CLONE, ES_EVENT_TYPE_AUTH_CREATE, ES_EVENT_TYPE_AUTH_EXCHANGEDATA,
|
||||
ES_EVENT_TYPE_AUTH_LINK, ES_EVENT_TYPE_AUTH_OPEN, ES_EVENT_TYPE_AUTH_RENAME,
|
||||
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK,
|
||||
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK, ES_EVENT_TYPE_NOTIFY_EXIT,
|
||||
};
|
||||
|
||||
#if HAVE_MACOS_12
|
||||
@@ -678,7 +767,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
logger:nullptr
|
||||
watchItems:nullptr
|
||||
enricher:nullptr
|
||||
decisionCache:nil];
|
||||
decisionCache:nil
|
||||
ttyWriter:nullptr];
|
||||
|
||||
EXPECT_CALL(*mockESApi, UnsubscribeAll);
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).WillOnce(testing::Return(true));
|
||||
@@ -689,12 +779,12 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testGetPathTargets {
|
||||
- (void)testPopulatePathTargets {
|
||||
// This test ensures that the `GetPathTargets` functions returns the
|
||||
// expected combination of targets for each handled event variant
|
||||
es_file_t testFile1 = MakeESFile("test_file_1");
|
||||
es_file_t testFile2 = MakeESFile("test_file_2");
|
||||
es_file_t testDir = MakeESFile("test_dir");
|
||||
es_file_t testFile1 = MakeESFile("test_file_1", MakeStat(100));
|
||||
es_file_t testFile2 = MakeESFile("test_file_2", MakeStat(200));
|
||||
es_file_t testDir = MakeESFile("test_dir", MakeStat(300));
|
||||
es_string_token_t testTok = MakeESStringToken("test_tok");
|
||||
std::string dirTok = std::string(testDir.path.data) + "/" + std::string(testTok.data);
|
||||
|
||||
@@ -715,6 +805,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertEqual(targets.size(), 1);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertTrue(targets[0].isReadable);
|
||||
XCTAssertEqual(targets[0].devnoIno.value(), FileID(testFile1));
|
||||
}
|
||||
|
||||
{
|
||||
@@ -729,8 +820,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertEqual(targets.size(), 2);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertFalse(targets[0].isReadable);
|
||||
XCTAssertFalse(targets[0].devnoIno.has_value());
|
||||
XCTAssertCppStringEqual(targets[1].path, dirTok);
|
||||
XCTAssertFalse(targets[1].isReadable);
|
||||
XCTAssertFalse(targets[1].devnoIno.has_value());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -747,8 +840,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertEqual(targets.size(), 2);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertFalse(targets[0].isReadable);
|
||||
XCTAssertFalse(targets[0].devnoIno.has_value());
|
||||
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
|
||||
XCTAssertFalse(targets[1].isReadable);
|
||||
XCTAssertFalse(targets[1].devnoIno.has_value());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -762,8 +857,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertEqual(targets.size(), 2);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertFalse(targets[0].isReadable);
|
||||
XCTAssertFalse(targets[0].devnoIno.has_value());
|
||||
XCTAssertCppStringEqual(targets[1].path, dirTok);
|
||||
XCTAssertFalse(targets[1].isReadable);
|
||||
XCTAssertFalse(targets[1].devnoIno.has_value());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -777,6 +874,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertEqual(targets.size(), 1);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertFalse(targets[0].isReadable);
|
||||
XCTAssertFalse(targets[0].devnoIno.has_value());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -791,8 +889,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertEqual(targets.size(), 2);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertTrue(targets[0].isReadable);
|
||||
XCTAssertEqual(targets[0].devnoIno.value(), FileID(testFile1));
|
||||
XCTAssertCppStringEqual(targets[1].path, dirTok);
|
||||
XCTAssertFalse(targets[1].isReadable);
|
||||
XCTAssertFalse(targets[1].devnoIno.has_value());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -806,8 +906,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertEqual(targets.size(), 2);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertFalse(targets[0].isReadable);
|
||||
XCTAssertFalse(targets[0].devnoIno.has_value());
|
||||
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
|
||||
XCTAssertFalse(targets[1].isReadable);
|
||||
XCTAssertFalse(targets[1].devnoIno.has_value());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -822,6 +924,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertEqual(targets.size(), 1);
|
||||
XCTAssertCppStringEqual(targets[0].path, dirTok);
|
||||
XCTAssertFalse(targets[0].isReadable);
|
||||
XCTAssertFalse(targets[0].devnoIno.has_value());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -834,6 +937,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertEqual(targets.size(), 1);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertFalse(targets[0].isReadable);
|
||||
XCTAssertFalse(targets[0].devnoIno.has_value());
|
||||
}
|
||||
|
||||
if (@available(macOS 12.0, *)) {
|
||||
@@ -852,8 +956,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertEqual(targets.size(), 2);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertTrue(targets[0].isReadable);
|
||||
XCTAssertEqual(targets[0].devnoIno.value(), FileID(testFile1));
|
||||
XCTAssertCppStringEqual(targets[1].path, dirTok);
|
||||
XCTAssertFalse(targets[1].isReadable);
|
||||
XCTAssertFalse(targets[1].devnoIno.has_value());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -865,8 +971,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertEqual(targets.size(), 2);
|
||||
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
|
||||
XCTAssertTrue(targets[0].isReadable);
|
||||
XCTAssertEqual(targets[0].devnoIno.value(), FileID(testFile1));
|
||||
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
|
||||
XCTAssertFalse(targets[1].isReadable);
|
||||
XCTAssertFalse(targets[1].devnoIno.has_value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/common/String.h"
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
@@ -44,6 +46,7 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
|
||||
@interface SNTEndpointSecurityRecorder ()
|
||||
@property SNTCompilerController *compilerController;
|
||||
@property SNTConfigurator *configurator;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityRecorder {
|
||||
@@ -69,6 +72,7 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
_compilerController = compilerController;
|
||||
_authResultCache = authResultCache;
|
||||
_prefixTree = prefixTree;
|
||||
_configurator = [SNTConfigurator configurator];
|
||||
|
||||
[self establishClientOrDie];
|
||||
}
|
||||
@@ -83,10 +87,24 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
|
||||
// Pre-enrichment processing
|
||||
switch (esMsg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE:
|
||||
// TODO(mlw): Once we move to building with the macOS 13 SDK, we should also check
|
||||
// the `was_mapped_writable` field
|
||||
if (esMsg->event.close.modified == false) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: {
|
||||
BOOL shouldLogClose = esMsg->event.close.modified;
|
||||
|
||||
#if HAVE_MACOS_13
|
||||
if (@available(macOS 13.5, *)) {
|
||||
// As of macSO 13.0 we have a new field for if a file was mmaped with
|
||||
// write permissions on close events. However it did not work until
|
||||
// 13.5.
|
||||
//
|
||||
// If something was mmaped writable it was probably written to. Often
|
||||
// developer tools do this to avoid lots of write syscalls, e.g. go's
|
||||
// tool chain. We log this so the compiler controller can take that into
|
||||
// account.
|
||||
shouldLogClose |= esMsg->event.close.was_mapped_writable;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!shouldLogClose) {
|
||||
// Ignore unmodified files
|
||||
// Note: Do not record metrics in this case. These are not considered "drops"
|
||||
// because this is not a failure case. Ideally we would tell ES to not send
|
||||
@@ -95,7 +113,22 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
}
|
||||
|
||||
self->_authResultCache->RemoveFromCache(esMsg->event.close.target);
|
||||
|
||||
// Only log file changes that match the given regex
|
||||
NSString *targetPath = santa::common::StringToNSString(esMsg->event.close.target->path.data);
|
||||
if (![[self.configurator fileChangesRegex]
|
||||
numberOfMatchesInString:targetPath
|
||||
options:0
|
||||
range:NSMakeRange(0, targetPath.length)]) {
|
||||
// Note: Do not record metrics in this case. These are not considered "drops"
|
||||
// because this is not a failure case.
|
||||
// TODO(mlw): Consider changes to configuration that would allow muting paths
|
||||
// to filter on the kernel side rather than in user space.
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,14 @@
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <objc/NSObjCRuntime.h>
|
||||
#include <cstddef>
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#include "Source/common/PrefixTree.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/common/Unit.h"
|
||||
#import "Source/santad/EventProviders/AuthResultCache.h"
|
||||
@@ -66,10 +68,21 @@ class MockLogger : public Logger {
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityRecorderTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityRecorderTest
|
||||
|
||||
- (void)setUp {
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
NSString *testPattern = @"^/foo/match.*";
|
||||
NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:testPattern
|
||||
options:0
|
||||
error:NULL];
|
||||
OCMStub([self.mockConfigurator fileChangesRegex]).andReturn(re);
|
||||
}
|
||||
|
||||
- (void)testEnable {
|
||||
// Ensure the client subscribes to expected event types
|
||||
std::set<es_event_type_t> expectedEventSubs{
|
||||
@@ -90,11 +103,21 @@ class MockLogger : public Logger {
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testHandleMessage {
|
||||
typedef void (^testHelperBlock)(es_message_t *message,
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient,
|
||||
std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
dispatch_semaphore_t *sema, dispatch_semaphore_t *semaMetrics);
|
||||
|
||||
es_file_t targetFileMatchesRegex = MakeESFile("/foo/matches");
|
||||
es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
|
||||
- (void)handleMessageWithMatchCalls:(BOOL)regexMatchCalls
|
||||
withMissCalls:(BOOL)regexFailsMatchCalls
|
||||
withBlock:(testHelperBlock)testBlock {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc, ActionType::Auth);
|
||||
es_file_t targetFile = MakeESFile("bar");
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
@@ -103,10 +126,15 @@ class MockLogger : public Logger {
|
||||
std::unique_ptr<EnrichedMessage> enrichedMsg = std::unique_ptr<EnrichedMessage>(nullptr);
|
||||
|
||||
auto mockEnricher = std::make_shared<MockEnricher>();
|
||||
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(std::move(enrichedMsg)));
|
||||
|
||||
if (regexMatchCalls) {
|
||||
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(std::move(enrichedMsg)));
|
||||
}
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFile)).Times(1);
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMatchesRegex)).Times((int)regexMatchCalls);
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMissesRegex))
|
||||
.Times((int)regexFailsMatchCalls);
|
||||
|
||||
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
|
||||
|
||||
@@ -114,11 +142,13 @@ class MockLogger : public Logger {
|
||||
// `SNTEndpointSecurityRecorder` object. There is a bug in OCMock that doesn't
|
||||
// properly handle the `processEnrichedMessage:handler:` block. Instead this
|
||||
// test will mock the `Log` method that is called in the handler block.
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
auto mockLogger = std::make_shared<MockLogger>(nullptr, nullptr);
|
||||
EXPECT_CALL(*mockLogger, Log).WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}));
|
||||
if (regexMatchCalls) {
|
||||
EXPECT_CALL(*mockLogger, Log).WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}));
|
||||
}
|
||||
|
||||
auto prefixTree = std::make_shared<PrefixTree<Unit>>();
|
||||
|
||||
@@ -133,54 +163,7 @@ class MockLogger : public Logger {
|
||||
authResultCache:mockAuthCache
|
||||
prefixTree:prefixTree];
|
||||
|
||||
// CLOSE not modified, bail early
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg.event.close.modified = false;
|
||||
esMsg.event.close.target = NULL;
|
||||
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, &esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTFail("Metrics record callback should not be called here");
|
||||
}]);
|
||||
}
|
||||
|
||||
// CLOSE modified, remove from cache
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg.event.close.modified = true;
|
||||
esMsg.event.close.target = &targetFile;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
|
||||
[recorderClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kProcessed);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertSemaTrue(sema, 5, "Log wasn't called within expected time window");
|
||||
}
|
||||
|
||||
// LINK, Prefix match, bail early
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_LINK;
|
||||
esMsg.event.link.source = &targetFile;
|
||||
prefixTree->InsertPrefix(esMsg.event.link.source->path.data, Unit{});
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
|
||||
[recorderClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
}
|
||||
testBlock(&esMsg, mockESApi, mockCC, recorderClient, prefixTree, &sema, &semaMetrics);
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockCC));
|
||||
|
||||
@@ -192,6 +175,150 @@ class MockLogger : public Logger {
|
||||
[mockCC stopMocking];
|
||||
}
|
||||
|
||||
- (void)testHandleMessageWithCloseMappedWriteable {
|
||||
#if HAVE_MACOS_13
|
||||
if (@available(macOS 13.0, *)) {
|
||||
// CLOSE not modified, but was_mapped_writable, should remove from cache,
|
||||
// and matches fileChangesRegex
|
||||
testHelperBlock testBlock =
|
||||
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema,
|
||||
__autoreleasing dispatch_semaphore_t *semaMetrics) {
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg->event.close.modified = false;
|
||||
esMsg->event.close.was_mapped_writable = true;
|
||||
esMsg->event.close.target = &targetFileMatchesRegex;
|
||||
Message msg(mockESApi, esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kProcessed);
|
||||
dispatch_semaphore_signal(*semaMetrics);
|
||||
}]);
|
||||
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertSemaTrue(*sema, 5, "Log wasn't called within expected time window");
|
||||
};
|
||||
|
||||
[self handleMessageWithMatchCalls:YES withMissCalls:NO withBlock:testBlock];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)testHandleEventCloseNotModifiedWithWasMappedWritable {
|
||||
#if HAVE_MACOS_13
|
||||
if (@available(macOS 13.0, *)) {
|
||||
// CLOSE not modified, but was_mapped_writable, remove from cache, and does not match
|
||||
// fileChangesRegex
|
||||
testHelperBlock testBlock =
|
||||
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema,
|
||||
__autoreleasing dispatch_semaphore_t *semaMetrics) {
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg->event.close.modified = false;
|
||||
esMsg->event.close.was_mapped_writable = true;
|
||||
esMsg->event.close.target = &targetFileMissesRegex;
|
||||
Message msg(mockESApi, esMsg);
|
||||
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTFail("Metrics record callback should not be called here");
|
||||
}]);
|
||||
};
|
||||
|
||||
[self handleMessageWithMatchCalls:NO withMissCalls:YES withBlock:testBlock];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)testHandleMessage {
|
||||
// CLOSE not modified, bail early
|
||||
testHelperBlock testBlock = ^(
|
||||
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg->event.close.modified = false;
|
||||
esMsg->event.close.target = NULL;
|
||||
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTFail("Metrics record callback should not be called here");
|
||||
}]);
|
||||
};
|
||||
|
||||
[self handleMessageWithMatchCalls:NO withMissCalls:NO withBlock:testBlock];
|
||||
|
||||
// CLOSE modified, remove from cache, and matches fileChangesRegex
|
||||
testBlock = ^(
|
||||
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg->event.close.modified = true;
|
||||
esMsg->event.close.target = &targetFileMatchesRegex;
|
||||
Message msg(mockESApi, esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
|
||||
[recorderClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kProcessed);
|
||||
dispatch_semaphore_signal(*semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertSemaTrue(*sema, 5, "Log wasn't called within expected time window");
|
||||
};
|
||||
|
||||
[self handleMessageWithMatchCalls:YES withMissCalls:NO withBlock:testBlock];
|
||||
|
||||
// CLOSE modified, remove from cache, but doesn't match fileChangesRegex
|
||||
testBlock = ^(
|
||||
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg->event.close.modified = true;
|
||||
esMsg->event.close.target = &targetFileMissesRegex;
|
||||
Message msg(mockESApi, esMsg);
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTFail("Metrics record callback should not be called here");
|
||||
}]);
|
||||
};
|
||||
|
||||
[self handleMessageWithMatchCalls:NO withMissCalls:YES withBlock:testBlock];
|
||||
|
||||
// LINK, Prefix match, bail early
|
||||
testBlock =
|
||||
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics)
|
||||
|
||||
{
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_LINK;
|
||||
esMsg->event.link.source = &targetFileMatchesRegex;
|
||||
prefixTree->InsertPrefix(esMsg->event.link.source->path.data, Unit{});
|
||||
Message msg(mockESApi, esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
|
||||
[recorderClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(*semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
};
|
||||
|
||||
[self handleMessageWithMatchCalls:NO withMissCalls:NO withBlock:testBlock];
|
||||
}
|
||||
|
||||
- (void)testGetTargetFileForPrefixTree {
|
||||
// Ensure `GetTargetFileForPrefixTree` returns expected field for each
|
||||
// subscribed event type in the `SNTEndpointSecurityRecorder`.
|
||||
|
||||
@@ -72,6 +72,11 @@ std::unique_ptr<Logger> Logger::Create(std::shared_ptr<EndpointSecurityAPI> esap
|
||||
Protobuf::Create(esapi, std::move(decision_cache)),
|
||||
Spool::Create([spool_log_path UTF8String], spool_dir_size_threshold,
|
||||
spool_file_size_threshold, spool_flush_timeout_ms));
|
||||
case SNTEventLogTypeJSON:
|
||||
return std::make_unique<Logger>(
|
||||
Protobuf::Create(esapi, std::move(decision_cache), true),
|
||||
File::Create(event_log_path, kFlushBufferTimeoutMS, kBufferBatchSizeBytes,
|
||||
kMaxExpectedWriteSizeBytes));
|
||||
default: LOGE(@"Invalid log type: %ld", log_type); return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,11 @@ class MockWriter : public Null {
|
||||
@"/tmp/spool", 1, 1, 1));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Protobuf>(logger.Serializer()));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Spool>(logger.Writer()));
|
||||
|
||||
logger = LoggerPeer(
|
||||
Logger::Create(mockESApi, SNTEventLogTypeJSON, nil, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Protobuf>(logger.Serializer()));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<File>(logger.Writer()));
|
||||
}
|
||||
|
||||
- (void)testLog {
|
||||
|
||||
@@ -55,6 +55,8 @@ class BasicString : public Serializer {
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated &) override;
|
||||
|
||||
std::vector<uint8_t> SerializeFileAccess(
|
||||
const std::string &policy_version, const std::string &policy_name,
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExec;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExit;
|
||||
@@ -46,6 +47,7 @@ using santa::santad::event_providers::endpoint_security::EnrichedProcess;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedRename;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::MountFromName;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::NonNull;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::Pid;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::Pidversion;
|
||||
@@ -411,6 +413,19 @@ std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedUnlink &msg) {
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedCSInvalidated &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=CODESIGNING_INVALIDATED");
|
||||
AppendProcess(str, esm.process);
|
||||
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
|
||||
msg.instigator().real_group());
|
||||
str.append("|codesigning_flags=");
|
||||
str.append([NSString stringWithFormat:@"0x%08x", esm.process->codesigning_flags].UTF8String);
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeFileAccess(const std::string &policy_version,
|
||||
const std::string &policy_name,
|
||||
const Message &msg,
|
||||
@@ -509,6 +524,8 @@ std::vector<uint8_t> BasicString::SerializeDiskAppeared(NSDictionary *props) {
|
||||
str.append([NonNull(dmg_path) UTF8String]);
|
||||
str.append("|appearance=");
|
||||
str.append([NonNull(appearanceDateString) UTF8String]);
|
||||
str.append("|mountfrom=");
|
||||
str.append([NonNull(MountFromName([props[@"DAVolumePath"] path])) UTF8String]);
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
@@ -251,6 +251,20 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageCSInvalidated {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED, &proc);
|
||||
|
||||
std::string got = BasicStringSerializeMessage(&esMsg);
|
||||
std::string want =
|
||||
"action=CODESIGNING_INVALIDATED"
|
||||
"|pid=12|ppid=56|process=foo|processpath=foo"
|
||||
"|uid=-2|user=nobody|gid=-1|group=nogroup|codesigning_flags=0x00000000|machineid=my_id\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testGetAccessTypeString {
|
||||
std::map<es_event_type_t, std::string> accessTypeToString = {
|
||||
{ES_EVENT_TYPE_AUTH_OPEN, "OPEN"}, {ES_EVENT_TYPE_AUTH_LINK, "LINK"},
|
||||
@@ -351,7 +365,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
@"DADeviceVendor" : @"vendor",
|
||||
@"DADeviceModel" : @"model",
|
||||
@"DAAppearanceTime" : @(1252487349), // 2009-09-09 09:09:09
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"path"],
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"/"],
|
||||
@"DAMediaBSDName" : @"bsd",
|
||||
@"DAVolumeKind" : @"apfs",
|
||||
@"DADeviceProtocol" : @"usb",
|
||||
@@ -365,11 +379,11 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
std::vector<uint8_t> ret = BasicString::Create(nullptr, nil, false)->SerializeDiskAppeared(props);
|
||||
std::string got(ret.begin(), ret.end());
|
||||
|
||||
std::string want = "action=DISKAPPEAR|mount=path|volume=|bsdname=bsd|fs=apfs"
|
||||
std::string want = "action=DISKAPPEAR|mount=/|volume=|bsdname=bsd|fs=apfs"
|
||||
"|model=vendor model|serial=|bus=usb|dmgpath="
|
||||
"|appearance=2040-09-09T09:09:09.000Z\n";
|
||||
"|appearance=2040-09-09T09:09:09.000Z|mountfrom=/";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
XCTAssertCppStringBeginsWith(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeDiskDisappeared {
|
||||
|
||||
@@ -47,6 +47,8 @@ class Empty : public Serializer {
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated &) override;
|
||||
|
||||
std::vector<uint8_t> SerializeFileAccess(
|
||||
const std::string &policy_version, const std::string &policy_name,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExec;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExit;
|
||||
@@ -65,6 +66,10 @@ std::vector<uint8_t> Empty::SerializeMessage(const EnrichedUnlink &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedCSInvalidated &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeFileAccess(const std::string &policy_version,
|
||||
const std::string &policy_name, const Message &msg,
|
||||
const EnrichedProcess &enriched_process,
|
||||
|
||||
@@ -43,6 +43,7 @@ namespace es = santa::santad::event_providers::endpoint_security;
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedLink *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedRename *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedUnlink *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedCSInvalidated *)&fake).size(), 0);
|
||||
|
||||
XCTAssertEqual(e->SerializeAllowlist(*(es::Message *)&fake, "").size(), 0);
|
||||
XCTAssertEqual(e->SerializeBundleHashingEvent(nil).size(), 0);
|
||||
|
||||
@@ -33,11 +33,11 @@ class Protobuf : public Serializer {
|
||||
public:
|
||||
static std::shared_ptr<Protobuf> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
SNTDecisionCache *decision_cache);
|
||||
SNTDecisionCache *decision_cache, bool json = false);
|
||||
|
||||
Protobuf(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
SNTDecisionCache *decision_cache);
|
||||
SNTDecisionCache *decision_cache, bool json = false);
|
||||
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
|
||||
@@ -56,6 +56,8 @@ class Protobuf : public Serializer {
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated &) override;
|
||||
|
||||
std::vector<uint8_t> SerializeFileAccess(
|
||||
const std::string &policy_version, const std::string &policy_name,
|
||||
@@ -87,6 +89,9 @@ class Protobuf : public Serializer {
|
||||
std::vector<uint8_t> FinalizeProto(::santa::pb::v1::SantaMessage *santa_msg);
|
||||
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
|
||||
// Toggle for transforming protobuf output to its JSON form.
|
||||
// See https://protobuf.dev/programming-guides/proto3/#json
|
||||
bool json_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <google/protobuf/json/json.h>
|
||||
#include <mach/message.h>
|
||||
#include <math.h>
|
||||
#include <sys/proc_info.h>
|
||||
@@ -34,14 +35,18 @@
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "google/protobuf/timestamp.pb.h"
|
||||
|
||||
using google::protobuf::Arena;
|
||||
using google::protobuf::Timestamp;
|
||||
using JsonPrintOptions = google::protobuf::json::PrintOptions;
|
||||
using google::protobuf::json::MessageToJsonString;
|
||||
|
||||
using santa::common::NSStringToUTF8StringView;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExec;
|
||||
@@ -55,6 +60,7 @@ using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::EffectiveGroup;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::EffectiveUser;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::MountFromName;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::NonNull;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::Pid;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::Pidversion;
|
||||
@@ -66,12 +72,13 @@ namespace pbv1 = ::santa::pb::v1;
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
std::shared_ptr<Protobuf> Protobuf::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTDecisionCache *decision_cache) {
|
||||
return std::make_shared<Protobuf>(esapi, std::move(decision_cache));
|
||||
SNTDecisionCache *decision_cache, bool json) {
|
||||
return std::make_shared<Protobuf>(esapi, std::move(decision_cache), json);
|
||||
}
|
||||
|
||||
Protobuf::Protobuf(std::shared_ptr<EndpointSecurityAPI> esapi, SNTDecisionCache *decision_cache)
|
||||
: Serializer(std::move(decision_cache)), esapi_(esapi) {}
|
||||
Protobuf::Protobuf(std::shared_ptr<EndpointSecurityAPI> esapi, SNTDecisionCache *decision_cache,
|
||||
bool json)
|
||||
: Serializer(std::move(decision_cache)), esapi_(esapi), json_(json) {}
|
||||
|
||||
static inline void EncodeTimestamp(Timestamp *timestamp, struct timespec ts) {
|
||||
timestamp->set_seconds(ts.tv_sec);
|
||||
@@ -387,6 +394,26 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::FinalizeProto(::pbv1::SantaMessage *santa_msg) {
|
||||
if (this->json_) {
|
||||
// TODO: Profile this. It's probably not the most efficient way to do this.
|
||||
JsonPrintOptions options;
|
||||
options.always_print_enums_as_ints = false;
|
||||
options.always_print_primitive_fields = true;
|
||||
options.preserve_proto_field_names = true;
|
||||
std::string json;
|
||||
|
||||
absl::Status status = MessageToJsonString(*santa_msg, &json, options);
|
||||
|
||||
if (!status.ok()) {
|
||||
LOGE(@"Failed to convert protobuf to JSON: %s", status.ToString().c_str());
|
||||
}
|
||||
|
||||
std::vector<uint8_t> vec(json.begin(), json.end());
|
||||
// Add a newline to the end of the JSON row.
|
||||
vec.push_back('\n');
|
||||
return vec;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> vec(santa_msg->ByteSizeLong());
|
||||
santa_msg->SerializeWithCachedSizesToArray(vec.data());
|
||||
return vec;
|
||||
@@ -574,6 +601,17 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedUnlink &msg) {
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedCSInvalidated &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::CodesigningInvalidated *pb_cs_invalidated = santa_msg->mutable_codesigning_invalidated();
|
||||
EncodeProcessInfoLight(pb_cs_invalidated->mutable_instigator(), msg.es_msg().version,
|
||||
msg.es_msg().process, msg.instigator());
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeFileAccess(const std::string &policy_version,
|
||||
const std::string &policy_name,
|
||||
const Message &msg,
|
||||
@@ -657,6 +695,8 @@ static void EncodeDisk(::pbv1::Disk *pb_disk, ::pbv1::Disk_Action action, NSDict
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_serial(); }, serial);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_bus(); }, props[@"DADeviceProtocol"]);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_dmg_path(); }, dmg_path);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_mount_from(); },
|
||||
MountFromName([props[@"DAVolumePath"] path]));
|
||||
|
||||
if (props[@"DAAppearanceTime"]) {
|
||||
// Note: `DAAppearanceTime` is set via `CFAbsoluteTimeGetCurrent`, which uses the defined
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <google/protobuf/json/json.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/proc_info.h>
|
||||
#include <sys/signal.h>
|
||||
@@ -26,8 +27,6 @@
|
||||
#include <uuid/uuid.h>
|
||||
#include <cstring>
|
||||
|
||||
#include <google/protobuf/util/json_util.h>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#include "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
@@ -41,11 +40,15 @@
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "google/protobuf/any.pb.h"
|
||||
#include "google/protobuf/timestamp.pb.h"
|
||||
|
||||
using google::protobuf::Timestamp;
|
||||
using google::protobuf::util::JsonPrintOptions;
|
||||
using JsonPrintOptions = google::protobuf::json::PrintOptions;
|
||||
using JsonParseOptions = ::google::protobuf::json::ParseOptions;
|
||||
using google::protobuf::json::JsonStringToMessage;
|
||||
using google::protobuf::json::MessageToJsonString;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
@@ -107,6 +110,7 @@ NSString *EventTypeToFilename(es_event_type_t eventType) {
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK: return @"link.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: return @"rename.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK: return @"unlink.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED: return @"cs_invalidated.json";
|
||||
default: XCTFail(@"Unhandled event type: %d", eventType); return nil;
|
||||
}
|
||||
}
|
||||
@@ -142,6 +146,7 @@ const google::protobuf::Message &SantaMessageEvent(const ::pbv1::SantaMessage &s
|
||||
case ::pbv1::SantaMessage::kBundle: return santaMsg.bundle();
|
||||
case ::pbv1::SantaMessage::kAllowlist: return santaMsg.allowlist();
|
||||
case ::pbv1::SantaMessage::kFileAccess: return santaMsg.file_access();
|
||||
case ::pbv1::SantaMessage::kCodesigningInvalidated: return santaMsg.codesigning_invalidated();
|
||||
case ::pbv1::SantaMessage::EVENT_NOT_SET:
|
||||
XCTFail(@"Protobuf message SantaMessage did not set an 'event' field");
|
||||
OS_FALLTHROUGH;
|
||||
@@ -157,14 +162,38 @@ std::string ConvertMessageToJsonString(const ::pbv1::SantaMessage &santaMsg) {
|
||||
const google::protobuf::Message &message = SantaMessageEvent(santaMsg);
|
||||
|
||||
std::string json;
|
||||
XCTAssertTrue(google::protobuf::util::MessageToJsonString(message, &json, options).ok());
|
||||
XCTAssertTrue(MessageToJsonString(message, &json, options).ok());
|
||||
return json;
|
||||
}
|
||||
|
||||
NSDictionary *findDelta(NSDictionary *a, NSDictionary *b) {
|
||||
NSMutableDictionary *delta = NSMutableDictionary.dictionary;
|
||||
|
||||
// Find objects in a that don't exist or are different in b.
|
||||
[a enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) {
|
||||
id otherObj = b[key];
|
||||
|
||||
if (![obj isEqual:otherObj]) {
|
||||
delta[key] = obj;
|
||||
}
|
||||
}];
|
||||
|
||||
// Find objects in the other dictionary that don't exist in self
|
||||
[b enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) {
|
||||
id aObj = a[key];
|
||||
|
||||
if (!aObj) {
|
||||
delta[key] = obj;
|
||||
}
|
||||
}];
|
||||
|
||||
return delta;
|
||||
}
|
||||
|
||||
void SerializeAndCheck(es_event_type_t eventType,
|
||||
void (^messageSetup)(std::shared_ptr<MockEndpointSecurityAPI>,
|
||||
es_message_t *),
|
||||
SNTDecisionCache *decisionCache) {
|
||||
SNTDecisionCache *decisionCache, bool json = false) {
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
for (uint32_t cur_version = 1; cur_version <= MaxSupportedESMessageVersionForCurrentOS();
|
||||
@@ -185,7 +214,7 @@ void SerializeAndCheck(es_event_type_t eventType,
|
||||
|
||||
messageSetup(mockESApi, &esMsg);
|
||||
|
||||
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi, decisionCache);
|
||||
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi, decisionCache, json);
|
||||
std::unique_ptr<EnrichedMessage> enrichedMsg = Enricher().Enrich(Message(mockESApi, &esMsg));
|
||||
|
||||
// Copy some values we need to check later before the object is moved out of this funciton
|
||||
@@ -204,14 +233,44 @@ void SerializeAndCheck(es_event_type_t eventType,
|
||||
std::vector<uint8_t> vec = bs->SerializeMessage(std::move(enrichedMsg));
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
// if we're checking against JSON then we should already have a jsonified string and just need
|
||||
// to
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
std::string gotData;
|
||||
|
||||
std::string gotData = ConvertMessageToJsonString(santaMsg);
|
||||
if (json) {
|
||||
// Parse the jsonified string into the protobuf
|
||||
// gotData = protoStr;
|
||||
JsonParseOptions options;
|
||||
options.ignore_unknown_fields = true;
|
||||
absl::Status status = JsonStringToMessage(protoStr, &santaMsg, options);
|
||||
XCTAssertTrue(status.ok());
|
||||
gotData = ConvertMessageToJsonString(santaMsg);
|
||||
} else {
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
gotData = ConvertMessageToJsonString(santaMsg);
|
||||
}
|
||||
|
||||
XCTAssertTrue(CompareTime(santaMsg.processed_time(), enrichmentTime));
|
||||
XCTAssertTrue(CompareTime(santaMsg.event_time(), msgTime));
|
||||
XCTAssertEqualObjects([NSString stringWithUTF8String:gotData.c_str()], wantData);
|
||||
|
||||
// Convert JSON strings to objects and compare each key-value set.
|
||||
NSError *jsonError;
|
||||
NSData *objectData = [wantData dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSDictionary *wantJSONDict =
|
||||
[NSJSONSerialization JSONObjectWithData:objectData
|
||||
options:NSJSONReadingMutableContainers
|
||||
error:&jsonError];
|
||||
XCTAssertNil(jsonError, @"failed to parse want data as JSON");
|
||||
NSDictionary *gotJSONDict = [NSJSONSerialization
|
||||
JSONObjectWithData:[NSData dataWithBytes:gotData.data() length:gotData.length()]
|
||||
options:NSJSONReadingMutableContainers
|
||||
error:&jsonError];
|
||||
XCTAssertNil(jsonError, @"failed to parse got data as JSON");
|
||||
|
||||
// XCTAssertEqualObjects([NSString stringWithUTF8String:gotData.c_str()], wantData);
|
||||
NSDictionary *delta = findDelta(wantJSONDict, gotJSONDict);
|
||||
XCTAssertEqualObjects(@{}, delta);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
@@ -294,8 +353,9 @@ void SerializeAndCheckNonESEvents(
|
||||
|
||||
- (void)serializeAndCheckEvent:(es_event_type_t)eventType
|
||||
messageSetup:(void (^)(std::shared_ptr<MockEndpointSecurityAPI>,
|
||||
es_message_t *))messageSetup {
|
||||
SerializeAndCheck(eventType, messageSetup, self.mockDecisionCache);
|
||||
es_message_t *))messageSetup
|
||||
json:(BOOL)json {
|
||||
SerializeAndCheck(eventType, messageSetup, self.mockDecisionCache, (bool)json);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageClose {
|
||||
@@ -306,7 +366,8 @@ void SerializeAndCheckNonESEvents(
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.close.modified = true;
|
||||
esMsg->event.close.target = &file;
|
||||
}];
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExchange {
|
||||
@@ -318,7 +379,8 @@ void SerializeAndCheckNonESEvents(
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.exchangedata.file1 = &file1;
|
||||
esMsg->event.exchangedata.file2 = &file2;
|
||||
}];
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testGetDecisionEnum {
|
||||
@@ -455,7 +517,60 @@ void SerializeAndCheckNonESEvents(
|
||||
.WillOnce(testing::Return(&fd2))
|
||||
.WillOnce(testing::Return(&fd3));
|
||||
}
|
||||
}];
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExecJSON {
|
||||
es_file_t procFileTarget = MakeESFile("fooexec", MakeStat(300));
|
||||
__block es_process_t procTarget =
|
||||
MakeESProcess(&procFileTarget, MakeAuditToken(23, 45), MakeAuditToken(67, 89));
|
||||
__block es_file_t fileCwd = MakeESFile("cwd", MakeStat(400));
|
||||
__block es_file_t fileScript = MakeESFile("script.sh", MakeStat(500));
|
||||
__block es_fd_t fd1 = {.fd = 1, .fdtype = PROX_FDTYPE_VNODE};
|
||||
__block es_fd_t fd2 = {.fd = 2, .fdtype = PROX_FDTYPE_SOCKET};
|
||||
__block es_fd_t fd3 = {.fd = 3, .fdtype = PROX_FDTYPE_PIPE, .pipe = {.pipe_id = 123}};
|
||||
|
||||
procTarget.codesigning_flags = CS_SIGNED | CS_HARD | CS_KILL;
|
||||
memset(procTarget.cdhash, 'A', sizeof(procTarget.cdhash));
|
||||
procTarget.signing_id = MakeESStringToken("my_signing_id");
|
||||
procTarget.team_id = MakeESStringToken("my_team_id");
|
||||
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_EXEC
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.exec.target = &procTarget;
|
||||
esMsg->event.exec.cwd = &fileCwd;
|
||||
esMsg->event.exec.script = &fileScript;
|
||||
|
||||
// For version 5, simulate a "truncated" set of FDs
|
||||
if (esMsg->version == 5) {
|
||||
esMsg->event.exec.last_fd = 123;
|
||||
} else {
|
||||
esMsg->event.exec.last_fd = 3;
|
||||
}
|
||||
|
||||
EXPECT_CALL(*mockESApi, ExecArgCount).WillOnce(testing::Return(3));
|
||||
EXPECT_CALL(*mockESApi, ExecArg)
|
||||
.WillOnce(testing::Return(MakeESStringToken("exec_path")))
|
||||
.WillOnce(testing::Return(MakeESStringToken("-l")))
|
||||
.WillOnce(testing::Return(MakeESStringToken("--foo")));
|
||||
|
||||
EXPECT_CALL(*mockESApi, ExecEnvCount).WillOnce(testing::Return(2));
|
||||
EXPECT_CALL(*mockESApi, ExecEnv)
|
||||
.WillOnce(
|
||||
testing::Return(MakeESStringToken("ENV_PATH=/path/to/bin:/and/another")))
|
||||
.WillOnce(testing::Return(MakeESStringToken("DEBUG=1")));
|
||||
|
||||
if (esMsg->version >= 4) {
|
||||
EXPECT_CALL(*mockESApi, ExecFDCount).WillOnce(testing::Return(3));
|
||||
EXPECT_CALL(*mockESApi, ExecFD)
|
||||
.WillOnce(testing::Return(&fd1))
|
||||
.WillOnce(testing::Return(&fd2))
|
||||
.WillOnce(testing::Return(&fd3));
|
||||
}
|
||||
}
|
||||
json:YES];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExit {
|
||||
@@ -463,7 +578,8 @@ void SerializeAndCheckNonESEvents(
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.exit.stat = W_EXITCODE(1, 0);
|
||||
}];
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testEncodeExitStatus {
|
||||
@@ -500,7 +616,8 @@ void SerializeAndCheckNonESEvents(
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.fork.child = &procChild;
|
||||
}];
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageLink {
|
||||
@@ -514,7 +631,8 @@ void SerializeAndCheckNonESEvents(
|
||||
esMsg->event.link.source = &fileSource;
|
||||
esMsg->event.link.target_dir = &fileTargetDir;
|
||||
esMsg->event.link.target_filename = targetTok;
|
||||
}];
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageRename {
|
||||
@@ -535,7 +653,8 @@ void SerializeAndCheckNonESEvents(
|
||||
esMsg->event.rename.destination.new_path.filename = targetTok;
|
||||
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
|
||||
}
|
||||
}];
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageUnlink {
|
||||
@@ -547,7 +666,16 @@ void SerializeAndCheckNonESEvents(
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.unlink.target = &fileTarget;
|
||||
esMsg->event.unlink.parent_dir = &fileTargetParent;
|
||||
}];
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageCodesigningInvalidated {
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testGetAccessType {
|
||||
@@ -653,7 +781,7 @@ void SerializeAndCheckNonESEvents(
|
||||
@"DADeviceVendor" : @"vendor",
|
||||
@"DADeviceModel" : @"model",
|
||||
@"DAAppearanceTime" : @(123456789),
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"path"],
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"/"],
|
||||
@"DAMediaBSDName" : @"bsd",
|
||||
@"DAVolumeKind" : @"apfs",
|
||||
@"DADeviceProtocol" : @"usb",
|
||||
@@ -678,6 +806,7 @@ void SerializeAndCheckNonESEvents(
|
||||
XCTAssertEqualObjects(@(pbDisk.serial().c_str()), @"");
|
||||
XCTAssertEqualObjects(@(pbDisk.bus().c_str()), props[@"DADeviceProtocol"]);
|
||||
XCTAssertEqualObjects(@(pbDisk.dmg_path().c_str()), @"");
|
||||
XCTAssertCppStringBeginsWith(pbDisk.mount_from(), std::string("/"));
|
||||
|
||||
// Note: `DAAppearanceTime` is treated as a reference time since 2001 and is converted to a
|
||||
// reference time of 1970. Skip the calculation in the test here, just ensure the value is set.
|
||||
|
||||
@@ -61,6 +61,8 @@ class Serializer {
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &) = 0;
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) = 0;
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated &) = 0;
|
||||
|
||||
virtual std::vector<uint8_t> SerializeFileAccess(
|
||||
const std::string &policy_version, const std::string &policy_name,
|
||||
@@ -95,6 +97,8 @@ class Serializer {
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &);
|
||||
std::vector<uint8_t> SerializeMessageTemplate(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &);
|
||||
std::vector<uint8_t> SerializeMessageTemplate(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated &);
|
||||
|
||||
bool enabled_machine_id_ = false;
|
||||
std::string machine_id_;
|
||||
|
||||
@@ -74,5 +74,8 @@ std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedRena
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedUnlink &msg) {
|
||||
return SerializeMessage(msg);
|
||||
}
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedCSInvalidated &msg) {
|
||||
return SerializeMessage(msg);
|
||||
}
|
||||
|
||||
}; // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
@@ -54,6 +54,8 @@ static inline NSString *NonNull(NSString *str) {
|
||||
NSString *OriginalPathForTranslocation(const es_process_t *es_proc);
|
||||
NSString *SerialForDevice(NSString *devPath);
|
||||
NSString *DiskImageForDevice(NSString *devPath);
|
||||
NSString *MountFromName(NSString *path);
|
||||
|
||||
es_file_t *GetAllowListTargetFile(
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg);
|
||||
|
||||
|
||||
@@ -14,9 +14,13 @@
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
|
||||
|
||||
#include <sys/mount.h>
|
||||
#include <sys/param.h>
|
||||
|
||||
#include "Source/common/SantaCache.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
#include "Source/common/SantaVnodeHash.h"
|
||||
#include "Source/common/String.h"
|
||||
|
||||
// These functions are exported by the Security framework, but are not included in headers
|
||||
extern "C" Boolean SecTranslocateIsTranslocatedURL(CFURLRef path, bool *isTranslocated,
|
||||
@@ -114,6 +118,22 @@ NSString *SerialForDevice(NSString *devPath) {
|
||||
return [serial stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
}
|
||||
|
||||
NSString *MountFromName(NSString *path) {
|
||||
if (!path.length) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
struct statfs sfs;
|
||||
|
||||
if (statfs(path.UTF8String, &sfs) != 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *mntFromName = santa::common::StringToNSString(sfs.f_mntfromname);
|
||||
|
||||
return mntFromName.length > 0 ? mntFromName : nil;
|
||||
}
|
||||
|
||||
NSString *DiskImageForDevice(NSString *devPath) {
|
||||
devPath = [devPath stringByDeletingLastPathComponent];
|
||||
if (!devPath.length) {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::GetAllowListTargetFile;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::MountFromName;
|
||||
|
||||
@interface UtilitiesTest : XCTestCase
|
||||
@end
|
||||
@@ -62,4 +63,12 @@ using santa::santad::logs::endpoint_security::serializers::Utilities::GetAllowLi
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testMountFromName {
|
||||
XCTAssertNil(MountFromName(@""));
|
||||
XCTAssertNil(MountFromName(nil));
|
||||
XCTAssertNil(MountFromName(@"./this/path/should/not/ever/exist/"));
|
||||
|
||||
XCTAssertCppStringBeginsWith(std::string(MountFromName(@"/").UTF8String), std::string("/"));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -10,6 +10,7 @@ proto_library(
|
||||
srcs = ["binaryproto.proto"],
|
||||
deps = [
|
||||
"@com_google_protobuf//:any_proto",
|
||||
"@com_google_protobuf//:timestamp_proto",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -69,6 +70,7 @@ santa_unit_test(
|
||||
deps = [
|
||||
":fsspool",
|
||||
":fsspool_log_batch_writer",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
@@ -156,6 +157,12 @@ std::string SpoolDirectory(absl::string_view base_dir) {
|
||||
return absl::StrCat(base_dir, PathSeparator(), kSpoolDirName);
|
||||
}
|
||||
|
||||
bool operator==(struct timespec a, struct timespec b) {
|
||||
return a.tv_sec == b.tv_sec && a.tv_nsec == b.tv_nsec;
|
||||
}
|
||||
|
||||
bool operator!=(struct timespec a, struct timespec b) { return !(a == b); }
|
||||
|
||||
} // namespace
|
||||
|
||||
FsSpoolWriter::FsSpoolWriter(absl::string_view base_dir, size_t max_spool_size)
|
||||
@@ -197,6 +204,25 @@ std::string FsSpoolWriter::UniqueFilename() {
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::StatusOr<size_t> FsSpoolWriter::EstimateSpoolDirSize() {
|
||||
struct stat stats;
|
||||
if (stat(spool_dir_.c_str(), &stats) < 0) {
|
||||
return absl::ErrnoToStatus(errno, "failed to stat spool directory");
|
||||
}
|
||||
|
||||
if (stats.st_mtimespec != spool_dir_last_mtime_) {
|
||||
// Store the updated mtime
|
||||
spool_dir_last_mtime_ = stats.st_mtimespec;
|
||||
|
||||
// Recompute the current estimated size
|
||||
return EstimateDirSize(spool_dir_);
|
||||
} else {
|
||||
// If the spool's last modification time hasn't changed then
|
||||
// re-use the current estimate.
|
||||
return spool_size_estimate_;
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status FsSpoolWriter::WriteMessage(absl::string_view msg) {
|
||||
if (absl::Status status = BuildDirectoryStructureIfNeeded(); !status.ok()) {
|
||||
return status; // << "can't create directory structure for writer";
|
||||
@@ -209,7 +235,7 @@ absl::Status FsSpoolWriter::WriteMessage(absl::string_view msg) {
|
||||
// Recompute the spool size if we think we are
|
||||
// over the limit.
|
||||
if (spool_size_estimate_ > max_spool_size_) {
|
||||
absl::StatusOr<size_t> estimate = EstimateDirSize(spool_dir_);
|
||||
absl::StatusOr<size_t> estimate = EstimateSpoolDirSize();
|
||||
if (!estimate.ok()) {
|
||||
return estimate.status(); // failed to recompute spool size
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
|
||||
// Forward declarations
|
||||
namespace fsspool {
|
||||
class FsSpoolWriterPeer;
|
||||
}
|
||||
|
||||
namespace fsspool {
|
||||
|
||||
// Enqueues messages into the spool. Multiple concurrent writers can
|
||||
@@ -42,10 +47,13 @@ class FsSpoolWriter {
|
||||
// returns the UNAVAILABLE canonical code (which is retryable).
|
||||
absl::Status WriteMessage(absl::string_view msg);
|
||||
|
||||
friend class fsspool::FsSpoolWriterPeer;
|
||||
|
||||
private:
|
||||
const std::string base_dir_;
|
||||
const std::string spool_dir_;
|
||||
const std::string tmp_dir_;
|
||||
struct timespec spool_dir_last_mtime_;
|
||||
|
||||
// Approximate maximum size of the spooling area, in bytes. If a message is
|
||||
// being written to a spooling area which already contains more than
|
||||
@@ -81,6 +89,10 @@ class FsSpoolWriter {
|
||||
// Generates a unique filename by combining the random ID of
|
||||
// this writer with a sequence number.
|
||||
std::string UniqueFilename();
|
||||
|
||||
// Estimate the size of the spool directory. However, only recompute a new
|
||||
// estimate if the spool directory has has a change to its modification time.
|
||||
absl::StatusOr<size_t> EstimateSpoolDirSize();
|
||||
};
|
||||
|
||||
// This class is thread-unsafe.
|
||||
|
||||
@@ -33,7 +33,6 @@ FsSpoolLogBatchWriter::~FsSpoolLogBatchWriter() {
|
||||
if (!s.ok()) {
|
||||
os_log(OS_LOG_DEFAULT, "Flush() failed with %s",
|
||||
s.ToString(absl::StatusToStringMode::kWithEverything).c_str());
|
||||
// LOG(WARNING) << "Flush() failed with " << s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +55,12 @@ absl::Status FsSpoolLogBatchWriter::FlushNoLock() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
cache_.mutable_records()->Clear();
|
||||
// We assign a new LogBatch() object here instead of calling Clear() method to
|
||||
// make sure the memory used by the cache_ is actually freed. It seems that
|
||||
// internal implementation of protobuf has some very generous way of managing
|
||||
// memory allocations and in certain scenarios it keeps objects for a very
|
||||
// long time (forever?).
|
||||
cache_ = santa::fsspool::binaryproto::LogBatch();
|
||||
cache_.mutable_records()->Reserve(max_batch_size_);
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ class FsSpoolLogBatchWriter {
|
||||
absl::Mutex cache_mutex_;
|
||||
santa::fsspool::binaryproto::LogBatch cache_ ABSL_GUARDED_BY(cache_mutex_);
|
||||
|
||||
absl::Status FlushNoLock() ABSL_SHARED_LOCKS_REQUIRED(cache_mutex_);
|
||||
absl::Status FlushNoLock() ABSL_EXCLUSIVE_LOCKS_REQUIRED(cache_mutex_);
|
||||
};
|
||||
|
||||
} // namespace fsspool
|
||||
|
||||
@@ -18,13 +18,31 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool_log_batch_writer.h"
|
||||
#include "google/protobuf/any.pb.h"
|
||||
#include "google/protobuf/timestamp.pb.h"
|
||||
|
||||
namespace fsspool {
|
||||
|
||||
class FsSpoolWriterPeer : public FsSpoolWriter {
|
||||
public:
|
||||
// Constructors
|
||||
using FsSpoolWriter::FsSpoolWriter;
|
||||
|
||||
// Private Methods
|
||||
using FsSpoolWriter::BuildDirectoryStructureIfNeeded;
|
||||
using FsSpoolWriter::EstimateSpoolDirSize;
|
||||
|
||||
// Private member variables
|
||||
using FsSpoolWriter::spool_size_estimate_;
|
||||
};
|
||||
|
||||
} // namespace fsspool
|
||||
|
||||
using fsspool::FsSpoolLogBatchWriter;
|
||||
using fsspool::FsSpoolWriter;
|
||||
using fsspool::FsSpoolWriterPeer;
|
||||
|
||||
static constexpr size_t kSpoolSize = 1048576;
|
||||
|
||||
@@ -72,8 +90,65 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
|
||||
XCTAssertTrue([self.fileMgr removeItemAtPath:self.testDir error:nil]);
|
||||
}
|
||||
|
||||
- (void)testEstimateSpoolDirSize {
|
||||
NSString *testData = @"What a day for some testing!";
|
||||
NSString *largeTestData = RepeatedString(@"A", 10240);
|
||||
NSString *path = [NSString stringWithFormat:@"%@/%@", self.spoolDir, @"temppy.log"];
|
||||
NSString *emptyPath = [NSString stringWithFormat:@"%@/%@", self.spoolDir, @"empty.log"];
|
||||
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
|
||||
|
||||
// Create the spool dir structure and ensure no files exist
|
||||
XCTAssertStatusOk(writer->BuildDirectoryStructureIfNeeded());
|
||||
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:nil] count], 0);
|
||||
|
||||
// Ensure that the initial spool dir estimate is 0
|
||||
auto status = writer->EstimateSpoolDirSize();
|
||||
XCTAssertStatusOk(status);
|
||||
XCTAssertEqual(*status, 0);
|
||||
|
||||
// Force the current estimate to be 0 since we're not recomputing on first write.
|
||||
writer->spool_size_estimate_ = *status;
|
||||
|
||||
XCTAssertTrue([testData writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil]);
|
||||
|
||||
// Ensure the test file was created
|
||||
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:nil] count], 1);
|
||||
|
||||
// Ensure the spool size estimate has grown at least as much as the content length
|
||||
status = writer->EstimateSpoolDirSize();
|
||||
XCTAssertStatusOk(status);
|
||||
// Update the current estimate
|
||||
writer->spool_size_estimate_ = *status;
|
||||
XCTAssertGreaterThanOrEqual(writer->spool_size_estimate_, testData.length);
|
||||
|
||||
// Modify file contents without modifying spool directory mtime
|
||||
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:path];
|
||||
[fileHandle seekToEndOfFile];
|
||||
[fileHandle writeData:[largeTestData dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
[fileHandle closeFile];
|
||||
|
||||
// Ensure only one file still exists
|
||||
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:nil] count], 1);
|
||||
|
||||
// Ensure that the returned estimate is the same as the old since mtime didn't change
|
||||
status = writer->EstimateSpoolDirSize();
|
||||
XCTAssertStatusOk(status);
|
||||
// Check that the current estimate is the same as the old estimate
|
||||
XCTAssertEqual(*status, writer->spool_size_estimate_);
|
||||
|
||||
// Create a second file in the spool dir to bump mtime
|
||||
XCTAssertTrue([@"" writeToFile:emptyPath atomically:YES encoding:NSUTF8StringEncoding error:nil]);
|
||||
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:nil] count], 2);
|
||||
|
||||
status = writer->EstimateSpoolDirSize();
|
||||
XCTAssertStatusOk(status);
|
||||
|
||||
// Ensure the newly returned size is appropriate
|
||||
XCTAssertGreaterThanOrEqual(*status, testData.length + largeTestData.length);
|
||||
}
|
||||
|
||||
- (void)testSimpleWrite {
|
||||
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
|
||||
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
|
||||
|
||||
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.baseDir]);
|
||||
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.spoolDir]);
|
||||
@@ -90,7 +165,7 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
|
||||
}
|
||||
|
||||
- (void)testSpoolFull {
|
||||
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
|
||||
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
|
||||
const std::string largeMessage(kSpoolSize + 1, '\x42');
|
||||
|
||||
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.baseDir]);
|
||||
@@ -121,7 +196,7 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
|
||||
}
|
||||
|
||||
- (void)testWriteMessageNoFlush {
|
||||
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
|
||||
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
|
||||
FsSpoolLogBatchWriter batch_writer(writer.get(), 10);
|
||||
|
||||
// Ensure that writing in batch mode doesn't flsuh on individual writes.
|
||||
@@ -134,7 +209,7 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
|
||||
|
||||
- (void)testWriteMessageFlushAtCapacity {
|
||||
static const int kCapacity = 5;
|
||||
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
|
||||
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
|
||||
FsSpoolLogBatchWriter batch_writer(writer.get(), kCapacity);
|
||||
|
||||
// Ensure batch flushed once capacity exceeded
|
||||
@@ -153,7 +228,7 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
|
||||
static const int kCapacity = 5;
|
||||
static const int kExpectedFlushes = 3;
|
||||
|
||||
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
|
||||
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
|
||||
FsSpoolLogBatchWriter batch_writer(writer.get(), kCapacity);
|
||||
|
||||
// Ensure batch flushed expected number of times
|
||||
@@ -173,7 +248,7 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
|
||||
static const int kCapacity = 10;
|
||||
static const int kNumberOfWrites = 7;
|
||||
|
||||
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
|
||||
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
|
||||
|
||||
{
|
||||
// Extra scope to enforce early destroy of batch_writer.
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
|
||||
namespace santa::santad {
|
||||
@@ -44,8 +46,18 @@ enum class Processor {
|
||||
kFileAccessAuthorizer,
|
||||
};
|
||||
|
||||
enum class FileAccessMetricStatus {
|
||||
kOK = 0,
|
||||
kBlockedUser,
|
||||
};
|
||||
|
||||
using EventCountTuple = std::tuple<Processor, es_event_type_t, EventDisposition>;
|
||||
using EventTimesTuple = std::tuple<Processor, es_event_type_t>;
|
||||
using FileAccessMetricsPolicyVersion = std::string;
|
||||
using FileAccessMetricsPolicyName = std::string;
|
||||
using FileAccessEventCountTuple =
|
||||
std::tuple<FileAccessMetricsPolicyVersion, FileAccessMetricsPolicyName, FileAccessMetricStatus,
|
||||
es_event_type_t, FileAccessPolicyDecision>;
|
||||
|
||||
class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
public:
|
||||
@@ -53,8 +65,8 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
|
||||
Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
|
||||
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
|
||||
SNTMetricCounter *rate_limit_counts, SNTMetricSet *metric_set,
|
||||
void (^run_on_first_start)(Metrics *));
|
||||
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *faa_event_counts,
|
||||
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *));
|
||||
|
||||
~Metrics();
|
||||
|
||||
@@ -71,6 +83,10 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
|
||||
void SetRateLimitingMetrics(Processor processor, int64_t events_rate_limited_count);
|
||||
|
||||
void SetFileAccessEventMetrics(std::string policy_version, std::string rule_name,
|
||||
FileAccessMetricStatus status, es_event_type_t event_type,
|
||||
FileAccessPolicyDecision decision);
|
||||
|
||||
friend class santa::santad::MetricsPeer;
|
||||
|
||||
private:
|
||||
@@ -84,6 +100,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
SNTMetricInt64Gauge *event_processing_times_;
|
||||
SNTMetricCounter *event_counts_;
|
||||
SNTMetricCounter *rate_limit_counts_;
|
||||
SNTMetricCounter *faa_event_counts_;
|
||||
SNTMetricSet *metric_set_;
|
||||
// Tracks whether or not the timer_source should be running.
|
||||
// This helps manage dispatch source state to ensure the source is not
|
||||
@@ -99,6 +116,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
std::map<EventCountTuple, int64_t> event_counts_cache_;
|
||||
std::map<EventTimesTuple, int64_t> event_times_cache_;
|
||||
std::map<Processor, int64_t> rate_limit_counts_cache_;
|
||||
std::map<FileAccessEventCountTuple, int64_t> faa_event_counts_cache_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad
|
||||
|
||||
@@ -53,6 +53,16 @@ static NSString *const kEventTypeNotifyUnmount = @"NotifyUnmount";
|
||||
static NSString *const kEventDispositionDropped = @"Dropped";
|
||||
static NSString *const kEventDispositionProcessed = @"Processed";
|
||||
|
||||
// Compat values
|
||||
static NSString *const kFileAccessMetricStatusOK = @"OK";
|
||||
static NSString *const kFileAccessMetricStatusBlockedUser = @"BLOCKED_USER";
|
||||
|
||||
static NSString *const kFileAccessPolicyDecisionDenied = @"Denied";
|
||||
static NSString *const kFileAccessPolicyDecisionDeniedInvalidSignature = @"Denied";
|
||||
static NSString *const kFileAccessPolicyDecisionAllowedAuditOnly = @"AllowedAuditOnly";
|
||||
|
||||
static NSString *const kFileAccessMetricsAccessType = @"access";
|
||||
|
||||
namespace santa::santad {
|
||||
|
||||
NSString *const ProcessorToString(Processor processor) {
|
||||
@@ -63,7 +73,8 @@ NSString *const ProcessorToString(Processor processor) {
|
||||
case Processor::kTamperResistance: return kProcessorTamperResistance;
|
||||
case Processor::kFileAccessAuthorizer: return kProcessorFileAccessAuthorizer;
|
||||
default:
|
||||
[NSException raise:@"Invalid processor" format:@"Unknown processor value: %d", processor];
|
||||
[NSException raise:@"Invalid processor"
|
||||
format:@"Unknown processor value: %d", static_cast<int>(processor)];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
@@ -103,7 +114,34 @@ NSString *const EventDispositionToString(EventDisposition d) {
|
||||
case EventDisposition::kDropped: return kEventDispositionDropped;
|
||||
case EventDisposition::kProcessed: return kEventDispositionProcessed;
|
||||
default:
|
||||
[NSException raise:@"Invalid disposition" format:@"Unknown disposition value: %d", d];
|
||||
[NSException raise:@"Invalid disposition"
|
||||
format:@"Unknown disposition value: %d", static_cast<int>(d)];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSString *const FileAccessMetricStatusToString(FileAccessMetricStatus status) {
|
||||
switch (status) {
|
||||
case FileAccessMetricStatus::kOK: return kFileAccessMetricStatusOK;
|
||||
case FileAccessMetricStatus::kBlockedUser: return kFileAccessMetricStatusBlockedUser;
|
||||
default:
|
||||
[NSException raise:@"Invalid file access metric status"
|
||||
format:@"Unknown file access metric status value: %d", static_cast<int>(status)];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSString *const FileAccessPolicyDecisionToString(FileAccessPolicyDecision decision) {
|
||||
switch (decision) {
|
||||
case FileAccessPolicyDecision::kDenied: return kFileAccessPolicyDecisionDenied;
|
||||
case FileAccessPolicyDecision::kDeniedInvalidSignature:
|
||||
return kFileAccessPolicyDecisionDeniedInvalidSignature;
|
||||
case FileAccessPolicyDecision::kAllowedAuditOnly:
|
||||
return kFileAccessPolicyDecisionAllowedAuditOnly;
|
||||
default:
|
||||
[NSException
|
||||
raise:@"Invalid file access policy decision"
|
||||
format:@"Unknown file access policy decision value: %d", static_cast<int>(decision)];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
@@ -129,9 +167,16 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
|
||||
fieldNames:@[ @"Processor" ]
|
||||
helpText:@"Events rate limited by each processor"];
|
||||
|
||||
SNTMetricCounter *faa_event_counts = [metric_set
|
||||
counterWithName:@"/santa/file_access_authorizer/log/count"
|
||||
fieldNames:@[
|
||||
@"config_version", @"access_type", @"rule_id", @"status", @"operation", @"decision"
|
||||
]
|
||||
helpText:@"Count of times a log is emitted from the File Access Authorizer client"];
|
||||
|
||||
std::shared_ptr<Metrics> metrics =
|
||||
std::make_shared<Metrics>(q, timer_source, interval, event_processing_times, event_counts,
|
||||
rate_limit_counts, metric_set, ^(Metrics *metrics) {
|
||||
rate_limit_counts, faa_event_counts, metric_set, ^(Metrics *metrics) {
|
||||
SNTRegisterCoreMetrics();
|
||||
metrics->EstablishConnection();
|
||||
});
|
||||
@@ -151,14 +196,15 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
|
||||
|
||||
Metrics::Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
|
||||
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
|
||||
SNTMetricCounter *rate_limit_counts, SNTMetricSet *metric_set,
|
||||
void (^run_on_first_start)(Metrics *))
|
||||
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *faa_event_counts,
|
||||
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *))
|
||||
: q_(q),
|
||||
timer_source_(timer_source),
|
||||
interval_(interval),
|
||||
event_processing_times_(event_processing_times),
|
||||
event_counts_(event_counts),
|
||||
rate_limit_counts_(rate_limit_counts),
|
||||
faa_event_counts_(faa_event_counts),
|
||||
metric_set_(metric_set),
|
||||
run_on_first_start_(run_on_first_start) {
|
||||
SetInterval(interval_);
|
||||
@@ -224,10 +270,26 @@ void Metrics::FlushMetrics() {
|
||||
[rate_limit_counts_ incrementBy:kv.second forFieldValues:@[ processorName ]];
|
||||
}
|
||||
|
||||
for (const auto &kv : faa_event_counts_cache_) {
|
||||
NSString *policyVersion = @(std::get<0>(kv.first).c_str()); // FileAccessMetricsPolicyVersion
|
||||
NSString *policyName = @(std::get<1>(kv.first).c_str()); // FileAccessMetricsPolicyName
|
||||
NSString *eventName = EventTypeToString(std::get<es_event_type_t>(kv.first));
|
||||
NSString *status = FileAccessMetricStatusToString(std::get<FileAccessMetricStatus>(kv.first));
|
||||
NSString *decision =
|
||||
FileAccessPolicyDecisionToString(std::get<FileAccessPolicyDecision>(kv.first));
|
||||
|
||||
[faa_event_counts_
|
||||
incrementBy:kv.second
|
||||
forFieldValues:@[
|
||||
policyVersion, kFileAccessMetricsAccessType, policyName, status, eventName, decision
|
||||
]];
|
||||
}
|
||||
|
||||
// Reset the maps so the next cycle begins with a clean state
|
||||
event_counts_cache_ = {};
|
||||
event_times_cache_ = {};
|
||||
rate_limit_counts_cache_ = {};
|
||||
faa_event_counts_cache_ = {};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -283,4 +345,13 @@ void Metrics::SetRateLimitingMetrics(Processor processor, int64_t events_rate_li
|
||||
});
|
||||
}
|
||||
|
||||
void Metrics::SetFileAccessEventMetrics(std::string policy_version, std::string rule_name,
|
||||
FileAccessMetricStatus status, es_event_type_t event_type,
|
||||
FileAccessPolicyDecision decision) {
|
||||
dispatch_sync(events_q_, ^{
|
||||
faa_event_counts_cache_[FileAccessEventCountTuple{
|
||||
std::move(policy_version), std::move(rule_name), status, event_type, decision}]++;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace santa::santad
|
||||
|
||||
@@ -24,37 +24,44 @@
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
using santa::santad::EventCountTuple;
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::EventTimesTuple;
|
||||
using santa::santad::FileAccessEventCountTuple;
|
||||
using santa::santad::Processor;
|
||||
|
||||
using EventCountTuple = std::tuple<Processor, es_event_type_t, EventDisposition>;
|
||||
using EventTimesTuple = std::tuple<Processor, es_event_type_t>;
|
||||
|
||||
namespace santa::santad {
|
||||
|
||||
extern NSString *const ProcessorToString(Processor processor);
|
||||
extern NSString *const EventTypeToString(es_event_type_t eventType);
|
||||
extern NSString *const EventDispositionToString(EventDisposition d);
|
||||
extern NSString *const FileAccessMetricStatusToString(FileAccessMetricStatus status);
|
||||
extern NSString *const FileAccessPolicyDecisionToString(FileAccessPolicyDecision decision);
|
||||
|
||||
class MetricsPeer : public Metrics {
|
||||
public:
|
||||
// Make base class constructors visible
|
||||
using Metrics::FlushMetrics;
|
||||
using Metrics::Metrics;
|
||||
|
||||
bool IsRunning() { return running_; }
|
||||
// Private methods
|
||||
using Metrics::FlushMetrics;
|
||||
|
||||
uint64_t Interval() { return interval_; }
|
||||
|
||||
std::map<EventCountTuple, int64_t> &EventCounts() { return event_counts_cache_; };
|
||||
std::map<EventTimesTuple, int64_t> &EventTimes() { return event_times_cache_; };
|
||||
std::map<Processor, int64_t> &RateLimitCounts() { return rate_limit_counts_cache_; };
|
||||
// Private member variables
|
||||
using Metrics::event_counts_cache_;
|
||||
using Metrics::event_times_cache_;
|
||||
using Metrics::faa_event_counts_cache_;
|
||||
using Metrics::interval_;
|
||||
using Metrics::rate_limit_counts_cache_;
|
||||
using Metrics::running_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad
|
||||
|
||||
using santa::santad::EventDispositionToString;
|
||||
using santa::santad::EventTypeToString;
|
||||
using santa::santad::FileAccessMetricStatus;
|
||||
using santa::santad::FileAccessMetricStatusToString;
|
||||
using santa::santad::FileAccessPolicyDecisionToString;
|
||||
using santa::santad::MetricsPeer;
|
||||
using santa::santad::ProcessorToString;
|
||||
|
||||
@@ -73,19 +80,19 @@ using santa::santad::ProcessorToString;
|
||||
|
||||
- (void)testStartStop {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m) {
|
||||
dispatch_semaphore_signal(self.sema);
|
||||
});
|
||||
|
||||
XCTAssertFalse(metrics->IsRunning());
|
||||
XCTAssertFalse(metrics->running_);
|
||||
|
||||
metrics->StartPoll();
|
||||
XCTAssertEqual(0, dispatch_semaphore_wait(self.sema, DISPATCH_TIME_NOW),
|
||||
"Initialization block never called");
|
||||
|
||||
// Should be marked running after starting
|
||||
XCTAssertTrue(metrics->IsRunning());
|
||||
XCTAssertTrue(metrics->running_);
|
||||
|
||||
metrics->StartPoll();
|
||||
|
||||
@@ -94,29 +101,29 @@ using santa::santad::ProcessorToString;
|
||||
"Initialization block called second time unexpectedly");
|
||||
|
||||
// Double-start doesn't change the running state
|
||||
XCTAssertTrue(metrics->IsRunning());
|
||||
XCTAssertTrue(metrics->running_);
|
||||
|
||||
metrics->StopPoll();
|
||||
|
||||
// After stopping, the internal state is no longer marked running
|
||||
XCTAssertFalse(metrics->IsRunning());
|
||||
XCTAssertFalse(metrics->running_);
|
||||
|
||||
metrics->StopPoll();
|
||||
|
||||
// Double-stop doesn't change the running state
|
||||
XCTAssertFalse(metrics->IsRunning());
|
||||
XCTAssertFalse(metrics->running_);
|
||||
}
|
||||
|
||||
- (void)testSetInterval {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
});
|
||||
|
||||
XCTAssertEqual(100, metrics->Interval());
|
||||
XCTAssertEqual(100, metrics->interval_);
|
||||
|
||||
metrics->SetInterval(200);
|
||||
XCTAssertEqual(200, metrics->Interval());
|
||||
XCTAssertEqual(200, metrics->interval_);
|
||||
}
|
||||
|
||||
- (void)testProcessorToString {
|
||||
@@ -179,25 +186,60 @@ using santa::santad::ProcessorToString;
|
||||
XCTAssertThrows(EventDispositionToString((EventDisposition)12345));
|
||||
}
|
||||
|
||||
- (void)testFileAccessMetricStatusToString {
|
||||
std::map<FileAccessMetricStatus, NSString *> statusToString = {
|
||||
{FileAccessMetricStatus::kOK, @"OK"},
|
||||
{FileAccessMetricStatus::kBlockedUser, @"BLOCKED_USER"},
|
||||
};
|
||||
|
||||
for (const auto &kv : statusToString) {
|
||||
XCTAssertEqualObjects(FileAccessMetricStatusToString(kv.first), kv.second);
|
||||
}
|
||||
|
||||
XCTAssertThrows(FileAccessMetricStatusToString((FileAccessMetricStatus)12345));
|
||||
}
|
||||
|
||||
- (void)testFileAccessPolicyDecisionToString {
|
||||
std::map<FileAccessPolicyDecision, NSString *> decisionToString = {
|
||||
{FileAccessPolicyDecision::kDenied, @"Denied"},
|
||||
{FileAccessPolicyDecision::kDeniedInvalidSignature, @"Denied"},
|
||||
{FileAccessPolicyDecision::kDeniedInvalidSignature, @"Denied"},
|
||||
};
|
||||
|
||||
for (const auto &kv : decisionToString) {
|
||||
XCTAssertEqualObjects(FileAccessPolicyDecisionToString(kv.first), kv.second);
|
||||
}
|
||||
|
||||
std::set<FileAccessPolicyDecision> decisionToStringThrows = {
|
||||
FileAccessPolicyDecision::kNoPolicy,
|
||||
FileAccessPolicyDecision::kAllowed,
|
||||
FileAccessPolicyDecision::kAllowedReadAccess,
|
||||
(FileAccessPolicyDecision)12345,
|
||||
};
|
||||
for (const auto &v : decisionToStringThrows) {
|
||||
XCTAssertThrows(FileAccessPolicyDecisionToString(v));
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testSetEventMetrics {
|
||||
int64_t nanos = 1234;
|
||||
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
|
||||
// Initial maps are empty
|
||||
XCTAssertEqual(metrics->EventCounts().size(), 0);
|
||||
XCTAssertEqual(metrics->EventTimes().size(), 0);
|
||||
XCTAssertEqual(metrics->event_counts_cache_.size(), 0);
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 0);
|
||||
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
|
||||
EventDisposition::kProcessed, nanos);
|
||||
|
||||
// Check sizes after setting metrics once
|
||||
XCTAssertEqual(metrics->EventCounts().size(), 1);
|
||||
XCTAssertEqual(metrics->EventTimes().size(), 1);
|
||||
XCTAssertEqual(metrics->event_counts_cache_.size(), 1);
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 1);
|
||||
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
|
||||
EventDisposition::kProcessed, nanos);
|
||||
@@ -205,8 +247,8 @@ using santa::santad::ProcessorToString;
|
||||
EventDisposition::kProcessed, nanos * 2);
|
||||
|
||||
// Re-check expected counts. One was an update, so should only be 2 items
|
||||
XCTAssertEqual(metrics->EventCounts().size(), 2);
|
||||
XCTAssertEqual(metrics->EventTimes().size(), 2);
|
||||
XCTAssertEqual(metrics->event_counts_cache_.size(), 2);
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 2);
|
||||
|
||||
// Check map values
|
||||
EventCountTuple ecExec{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
|
||||
@@ -216,36 +258,72 @@ using santa::santad::ProcessorToString;
|
||||
EventTimesTuple etExec{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC};
|
||||
EventTimesTuple etOpen{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN};
|
||||
|
||||
XCTAssertEqual(metrics->EventCounts()[ecExec], 2);
|
||||
XCTAssertEqual(metrics->EventCounts()[ecOpen], 1);
|
||||
XCTAssertEqual(metrics->EventTimes()[etExec], nanos);
|
||||
XCTAssertEqual(metrics->EventTimes()[etOpen], nanos * 2);
|
||||
XCTAssertEqual(metrics->event_counts_cache_[ecExec], 2);
|
||||
XCTAssertEqual(metrics->event_counts_cache_[ecOpen], 1);
|
||||
XCTAssertEqual(metrics->event_times_cache_[etExec], nanos);
|
||||
XCTAssertEqual(metrics->event_times_cache_[etOpen], nanos * 2);
|
||||
}
|
||||
|
||||
- (void)testSetRateLimitingMetrics {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
|
||||
// Initial map is empty
|
||||
XCTAssertEqual(metrics->RateLimitCounts().size(), 0);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 0);
|
||||
|
||||
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 123);
|
||||
|
||||
// Check sizes after setting metrics once
|
||||
XCTAssertEqual(metrics->RateLimitCounts().size(), 1);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 1);
|
||||
|
||||
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 456);
|
||||
metrics->SetRateLimitingMetrics(Processor::kAuthorizer, 789);
|
||||
|
||||
// Re-check expected counts. One was an update, so should only be 2 items
|
||||
XCTAssertEqual(metrics->RateLimitCounts().size(), 2);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 2);
|
||||
|
||||
// Check map values
|
||||
XCTAssertEqual(metrics->RateLimitCounts()[Processor::kFileAccessAuthorizer], 123 + 456);
|
||||
XCTAssertEqual(metrics->RateLimitCounts()[Processor::kAuthorizer], 789);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_[Processor::kFileAccessAuthorizer], 123 + 456);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_[Processor::kAuthorizer], 789);
|
||||
}
|
||||
|
||||
- (void)testSetFileAccessEventMetrics {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
|
||||
// Initial map is empty
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 0);
|
||||
|
||||
metrics->SetFileAccessEventMetrics("v1.0", "rule_abc", FileAccessMetricStatus::kOK,
|
||||
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied);
|
||||
|
||||
// Check sizes after setting metrics once
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 1);
|
||||
|
||||
// Update the previous metric
|
||||
metrics->SetFileAccessEventMetrics("v1.0", "rule_abc", FileAccessMetricStatus::kOK,
|
||||
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied);
|
||||
|
||||
// Add a second metric
|
||||
metrics->SetFileAccessEventMetrics("v1.0", "rule_xyz", FileAccessMetricStatus::kOK,
|
||||
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied);
|
||||
|
||||
// Re-check expected counts. One was an update, so should only be 2 items
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 2);
|
||||
|
||||
FileAccessEventCountTuple ruleAbc{"v1.0", "rule_abc", FileAccessMetricStatus::kOK,
|
||||
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied};
|
||||
FileAccessEventCountTuple ruleXyz{"v1.0", "rule_xyz", FileAccessMetricStatus::kOK,
|
||||
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied};
|
||||
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_[ruleAbc], 2);
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_[ruleXyz], 1);
|
||||
}
|
||||
|
||||
- (void)testFlushMetrics {
|
||||
@@ -266,22 +344,26 @@ using santa::santad::ProcessorToString;
|
||||
});
|
||||
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, mockEventProcessingTimes,
|
||||
mockEventCounts, mockEventCounts, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
auto metrics =
|
||||
std::make_shared<MetricsPeer>(self.q, timer, 100, mockEventProcessingTimes, mockEventCounts,
|
||||
mockEventCounts, mockEventCounts, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
|
||||
EventDisposition::kProcessed, nanos);
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN,
|
||||
EventDisposition::kProcessed, nanos * 2);
|
||||
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 123);
|
||||
metrics->SetFileAccessEventMetrics("v1.0", "rule_abc", FileAccessMetricStatus::kOK,
|
||||
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied);
|
||||
|
||||
// First ensure we have the expected map sizes
|
||||
XCTAssertEqual(metrics->EventCounts().size(), 2);
|
||||
XCTAssertEqual(metrics->EventTimes().size(), 2);
|
||||
XCTAssertEqual(metrics->RateLimitCounts().size(), 1);
|
||||
XCTAssertEqual(metrics->event_counts_cache_.size(), 2);
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 2);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 1);
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 1);
|
||||
|
||||
metrics->FlushMetrics();
|
||||
|
||||
@@ -293,11 +375,13 @@ using santa::santad::ProcessorToString;
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (3)");
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (4)");
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (5)");
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (6)");
|
||||
|
||||
// After a flush, map sizes should be reset to 0
|
||||
XCTAssertEqual(metrics->EventCounts().size(), 0);
|
||||
XCTAssertEqual(metrics->EventTimes().size(), 0);
|
||||
XCTAssertEqual(metrics->RateLimitCounts().size(), 0);
|
||||
XCTAssertEqual(metrics->event_counts_cache_.size(), 0);
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 0);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 0);
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 0);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -156,6 +156,21 @@ double watchdogRAMPeak = 0;
|
||||
reply([SNTConfigurator configurator].staticRules.count);
|
||||
}
|
||||
|
||||
- (void)retrieveAllRules:(void (^)(NSArray<SNTRule *> *, NSError *))reply {
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
|
||||
// Do not return any rules if syncBaseURL is set and return an error.
|
||||
if (config.syncBaseURL) {
|
||||
reply(@[], [NSError errorWithDomain:@"com.google.santad"
|
||||
code:403 // (TODO) define error code
|
||||
userInfo:@{NSLocalizedDescriptionKey : @"SyncBaseURL is set"}]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray<SNTRule *> *rules = [[SNTDatabaseController ruleTable] retrieveAllRules];
|
||||
reply(rules, nil);
|
||||
}
|
||||
|
||||
#pragma mark Decision Ops
|
||||
|
||||
- (void)decisionForFilePath:(NSString *)filePath
|
||||
@@ -252,6 +267,11 @@ double watchdogRAMPeak = 0;
|
||||
reply();
|
||||
}
|
||||
|
||||
- (void)setOverrideFileAccessAction:(NSString *)action reply:(void (^)(void))reply {
|
||||
[[SNTConfigurator configurator] setSyncServerOverrideFileAccessAction:action];
|
||||
reply();
|
||||
}
|
||||
|
||||
- (void)enableBundles:(void (^)(BOOL))reply {
|
||||
reply([SNTConfigurator configurator].enableBundles);
|
||||
}
|
||||
|
||||
@@ -85,8 +85,8 @@ SNTCachedDecision *MakeCachedDecision(struct stat sb, SNTEventState decision) {
|
||||
|
||||
OCMExpect([self.mockRuleDatabase
|
||||
resetTimestampForRule:[OCMArg checkWithBlock:^BOOL(SNTRule *rule) {
|
||||
return rule.identifier == cd.sha256 && rule.state == SNTRuleStateAllowTransitive &&
|
||||
rule.type == SNTRuleTypeBinary;
|
||||
return [rule.identifier isEqualToString:cd.sha256] &&
|
||||
rule.state == SNTRuleStateAllowTransitive && rule.type == SNTRuleTypeBinary;
|
||||
}]]);
|
||||
|
||||
[dc resetTimestampForCachedDecision:sb];
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/TTYWriter.h"
|
||||
|
||||
const static NSString *kBlockBinary = @"BlockBinary";
|
||||
const static NSString *kAllowBinary = @"AllowBinary";
|
||||
@@ -56,7 +56,8 @@ const static NSString *kBlockLongPath = @"BlockLongPath";
|
||||
- (instancetype)initWithRuleTable:(SNTRuleTable *)ruleTable
|
||||
eventTable:(SNTEventTable *)eventTable
|
||||
notifierQueue:(SNTNotificationQueue *)notifierQueue
|
||||
syncdQueue:(SNTSyncdQueue *)syncdQueue;
|
||||
syncdQueue:(SNTSyncdQueue *)syncdQueue
|
||||
ttyWriter:(std::shared_ptr<santa::santad::TTYWriter>)ttyWriter;
|
||||
|
||||
///
|
||||
/// Handles the logic of deciding whether to allow the binary to run or not, sends the response to
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#import "Source/santad/SNTExecutionController.h"
|
||||
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <copyfile.h>
|
||||
#include <libproc.h>
|
||||
@@ -22,7 +23,7 @@
|
||||
#include <sys/param.h>
|
||||
#include <utmpx.h>
|
||||
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#include <memory>
|
||||
|
||||
#include "Source/common/BranchPrediction.h"
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
@@ -44,6 +45,7 @@
|
||||
#import "Source/santad/SNTPolicyProcessor.h"
|
||||
#import "Source/santad/SNTSyncdQueue.h"
|
||||
|
||||
using santa::santad::TTYWriter;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
static const size_t kMaxAllowedPathLength = MAXPATHLEN - 1; // -1 to account for null terminator
|
||||
@@ -59,7 +61,9 @@ static const size_t kMaxAllowedPathLength = MAXPATHLEN - 1; // -1 to account fo
|
||||
@property dispatch_queue_t eventQueue;
|
||||
@end
|
||||
|
||||
@implementation SNTExecutionController
|
||||
@implementation SNTExecutionController {
|
||||
std::shared_ptr<TTYWriter> _ttyWriter;
|
||||
}
|
||||
|
||||
static NSString *const kPrinterProxyPreMonterey =
|
||||
(@"/System/Library/Frameworks/Carbon.framework/Versions/Current/"
|
||||
@@ -74,13 +78,15 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
- (instancetype)initWithRuleTable:(SNTRuleTable *)ruleTable
|
||||
eventTable:(SNTEventTable *)eventTable
|
||||
notifierQueue:(SNTNotificationQueue *)notifierQueue
|
||||
syncdQueue:(SNTSyncdQueue *)syncdQueue {
|
||||
syncdQueue:(SNTSyncdQueue *)syncdQueue
|
||||
ttyWriter:(std::shared_ptr<TTYWriter>)ttyWriter {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_ruleTable = ruleTable;
|
||||
_eventTable = eventTable;
|
||||
_notifierQueue = notifierQueue;
|
||||
_syncdQueue = syncdQueue;
|
||||
_ttyWriter = std::move(ttyWriter);
|
||||
_policyProcessor = [[SNTPolicyProcessor alloc] initWithRuleTable:_ruleTable];
|
||||
|
||||
_eventQueue =
|
||||
@@ -225,7 +231,7 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
// Respond with the decision.
|
||||
postAction(action);
|
||||
|
||||
// Increment counters;
|
||||
// Increment metric counters
|
||||
[self incrementEventCounters:cd.decision];
|
||||
|
||||
// Log to database if necessary.
|
||||
@@ -294,26 +300,28 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
}
|
||||
|
||||
if (!cd.silentBlock) {
|
||||
// Let the user know what happened, both on the terminal and in the GUI.
|
||||
NSAttributedString *s = [SNTBlockMessage attributedBlockMessageForEvent:se
|
||||
customMessage:cd.customMsg];
|
||||
if (!config.enableSilentTTYMode && self->_ttyWriter && TTYWriter::CanWrite(targetProc)) {
|
||||
// Let the user know what happened on the terminal
|
||||
NSAttributedString *s = [SNTBlockMessage attributedBlockMessageForEvent:se
|
||||
customMessage:cd.customMsg];
|
||||
|
||||
if (targetProc->tty && targetProc->tty->path.length > 0 && !config.enableSilentTTYMode) {
|
||||
NSMutableString *msg = [NSMutableString stringWithCapacity:1024];
|
||||
// Escape sequences `\033[1m` and `\033[0m` begin/end bold lettering
|
||||
[msg appendFormat:@"\n\033[1mSanta\033[0m\n\n%@\n\n", s.string];
|
||||
[msg appendFormat:@"\033[1mPath: \033[0m %@\n"
|
||||
@"\033[1mIdentifier:\033[0m %@\n"
|
||||
@"\033[1mParent: \033[0m %@ (%@)\n\n",
|
||||
se.filePath, se.fileSHA256, se.parentName, se.ppid];
|
||||
NSURL *detailURL = [SNTBlockMessage eventDetailURLForEvent:se];
|
||||
NSURL *detailURL = [SNTBlockMessage eventDetailURLForEvent:se customURL:cd.customURL];
|
||||
if (detailURL) {
|
||||
[msg appendFormat:@"More info:\n%@\n\n", detailURL.absoluteString];
|
||||
}
|
||||
|
||||
[self printMessage:msg toTTY:targetProc->tty->path.data];
|
||||
self->_ttyWriter->Write(targetProc, msg);
|
||||
}
|
||||
|
||||
[self.notifierQueue addEvent:se customMessage:cd.customMsg];
|
||||
// Let the user know what happened in the GUI.
|
||||
[self.notifierQueue addEvent:se withCustomMessage:cd.customMsg andCustomURL:cd.customURL];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,13 +371,6 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
return proxyInfo;
|
||||
}
|
||||
|
||||
- (void)printMessage:(NSString *)msg toTTY:(const char *)path {
|
||||
int fd = open(path, O_WRONLY | O_NOCTTY);
|
||||
std::string_view str = santa::common::NSStringToUTF8StringView(msg);
|
||||
write(fd, str.data(), str.length());
|
||||
close(fd);
|
||||
}
|
||||
|
||||
- (void)loggedInUsers:(NSArray **)users sessions:(NSArray **)sessions {
|
||||
NSMutableSet *loggedInUsers = [NSMutableSet set];
|
||||
NSMutableArray *loggedInHosts = [NSMutableArray array];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user