mirror of
https://github.com/google/santa.git
synced 2026-01-15 09:17:59 -05:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e124f4c51 | ||
|
|
cd719ccef4 | ||
|
|
dde42ee686 | ||
|
|
d144e27798 | ||
|
|
afc2c216b8 | ||
|
|
03d7556f22 | ||
|
|
020827b091 | ||
|
|
baa31a5db0 | ||
|
|
9ba7075596 | ||
|
|
5d08538639 | ||
|
|
e73bafb596 | ||
|
|
1e92d109a7 | ||
|
|
6a6aa6dce8 | ||
|
|
0715033d6a | ||
|
|
123d7a2d6a | ||
|
|
7b4d997589 | ||
|
|
5307bd9b7f | ||
|
|
0622e6de71 | ||
|
|
e7c32ae87d | ||
|
|
deaf3a638c | ||
|
|
8a7f1142a8 | ||
|
|
c180205059 | ||
|
|
337df0aa31 | ||
|
|
e2b099aa50 | ||
|
|
fc4e29f34c | ||
|
|
bf3b6bc6e2 | ||
|
|
b810fc81e1 | ||
|
|
3b3aa999c5 | ||
|
|
59428f3be3 | ||
|
|
ae6451a9b2 | ||
|
|
feac080fa7 | ||
|
|
d0f2a0ac4d | ||
|
|
7fc06ea9d8 | ||
|
|
1dfeeac936 | ||
|
|
ac9b5d9399 | ||
|
|
7f3f1c5448 | ||
|
|
46efd6893f | ||
|
|
50232578d6 | ||
|
|
d83be03a20 | ||
|
|
119b29b534 | ||
|
|
be87b3eaf2 | ||
|
|
0fe672817e | ||
|
|
c3b2fbf512 | ||
|
|
2984d98cb9 | ||
|
|
5295faef0e | ||
|
|
0209344f62 | ||
|
|
53ca5eb811 | ||
|
|
33c7aab9f1 | ||
|
|
f6d837ac31 | ||
|
|
5e0a383662 | ||
|
|
8055b451bb | ||
|
|
c5e7736eef |
10
.bazelrc
10
.bazelrc
@@ -3,6 +3,10 @@ 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
|
||||
|
||||
@@ -21,20 +25,20 @@ build:asan --config=san-common
|
||||
build:asan --copt="-fsanitize=address"
|
||||
build:asan --copt="-DADDRESS_SANITIZER"
|
||||
build:asan --linkopt="-fsanitize=address"
|
||||
build:asan --action_env="ASAN_OPTIONS=log_path=/tmp/san_out"
|
||||
build:asan --test_env="ASAN_OPTIONS=log_path=/tmp/san_out"
|
||||
|
||||
build:tsan --config=san-common
|
||||
build:tsan --copt="-fsanitize=thread"
|
||||
build:tsan --copt="-DTHREAD_SANITIZER=1"
|
||||
build:tsan --linkopt="-fsanitize=thread"
|
||||
build:asan --action_env="TSAN_OPTIONS=log_path=/tmp/san_out"
|
||||
build:tsan --test_env="TSAN_OPTIONS=log_path=/tmp/san_out:halt_on_error=true"
|
||||
|
||||
build:ubsan --config=san-common
|
||||
build:ubsan --copt="-fsanitize=undefined"
|
||||
build:ubsan --copt="-DUNDEFINED_SANITIZER=1"
|
||||
build:ubsan --copt="-fno-sanitize=function" --copt="-fno-sanitize=vptr"
|
||||
build:ubsan --linkopt="-fsanitize=undefined"
|
||||
build:ubsan --action_env="UBSAN_OPTIONS=log_path=/tmp/san_out"
|
||||
build:ubsan --test_env="UBSAN_OPTIONS=log_path=/tmp/san_out"
|
||||
|
||||
build:fuzz --config=san-common
|
||||
build:fuzz --@rules_fuzzing//fuzzing:cc_engine=@rules_fuzzing//fuzzing/engines:libfuzzer
|
||||
|
||||
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run linters
|
||||
run: ./Testing/lint.sh
|
||||
|
||||
@@ -24,10 +24,10 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-11, macos-12]
|
||||
os: [macos-11, macos-12, macos-13]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build Userspace
|
||||
run: bazel build --apple_generate_dsym -c opt :release --define=SANTA_BUILD_TYPE=adhoc
|
||||
|
||||
@@ -35,17 +35,17 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-11, macos-12]
|
||||
os: [macos-11, macos-12, macos-13]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run All Tests
|
||||
run: bazel test :unit_tests --define=SANTA_BUILD_TYPE=adhoc --test_output=errors
|
||||
|
||||
test_coverage:
|
||||
runs-on: macos-11
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Generate test coverage
|
||||
run: sh ./generate_cov.sh
|
||||
- name: Coveralls
|
||||
|
||||
2
.github/workflows/continuous.yml
vendored
2
.github/workflows/continuous.yml
vendored
@@ -8,6 +8,6 @@ jobs:
|
||||
preqs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checks for flaky tests
|
||||
run: bazel test --test_strategy=exclusive --test_output=errors --runs_per_test 50 -t- :unit_tests --define=SANTA_BUILD_TYPE=adhoc
|
||||
|
||||
4
.github/workflows/e2e.yml
vendored
4
.github/workflows/e2e.yml
vendored
@@ -6,7 +6,7 @@ jobs:
|
||||
start_vm:
|
||||
runs-on: e2e-host
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Start VM
|
||||
run: python3 Testing/integration/actions/start_vm.py macOS_12.bundle.tar.gz
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
env:
|
||||
VM_PASSWORD: ${{ secrets.VM_PASSWORD }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install configuration profile
|
||||
run: bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig
|
||||
- name: Add homebrew to PATH
|
||||
|
||||
4
.github/workflows/fuzz.yml
vendored
4
.github/workflows/fuzz.yml
vendored
@@ -9,14 +9,14 @@ jobs:
|
||||
start_vm:
|
||||
runs-on: e2e-host
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Start VM
|
||||
run: python3 Testing/integration/actions/start_vm.py macOS_13.bundle.tar.gz
|
||||
|
||||
fuzz:
|
||||
runs-on: e2e-vm
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup libfuzzer
|
||||
run: Fuzzing/install_libclang_fuzzer.sh
|
||||
- name: Fuzz
|
||||
|
||||
4
.github/workflows/sanitizers.yml
vendored
4
.github/workflows/sanitizers.yml
vendored
@@ -11,14 +11,14 @@ jobs:
|
||||
matrix:
|
||||
sanitizer: [asan, tsan, ubsan]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: ${{ matrix.sanitizer }}
|
||||
run: |
|
||||
CLANG_VERSION=$(clang --version | head -n 1 | cut -d' ' -f 4)
|
||||
DYLIB_PATH="$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/${CLANG_VERSION}/lib/darwin/libclang_rt.${{ matrix.sanitizer }}_osx_dynamic.dylib"
|
||||
|
||||
bazel test --config=${{ matrix.sanitizer }} \
|
||||
--test_strategy=exclusive --test_output=errors \
|
||||
--test_strategy=exclusive --test_output=all \
|
||||
--test_env=DYLD_INSERT_LIBRARIES=${DYLIB_PATH} \
|
||||
--runs_per_test 5 -t- :unit_tests \
|
||||
--define=SANTA_BUILD_TYPE=adhoc
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
# Example NOTARIZATION_TOOL wrapper.
|
||||
|
||||
/usr/bin/xcrun altool --notarize-app "${2}" --primary-bundle-id "${4}" \
|
||||
-u "${NOTARIZATION_USERNAME}" -p "${NOTARIZATION_PASSWORD}"
|
||||
/usr/bin/xcrun notarytool submit "${2}" --wait \
|
||||
--apple-id "${NOTARIZATION_USERNAME}" --password "${NOTARIZATION_PASSWORD}"
|
||||
|
||||
@@ -28,8 +28,6 @@
|
||||
# tool around the tool to use for notarization. The tool must take 2 flags:
|
||||
# --file
|
||||
# - pointing at a zip file containing the artifact to notarize
|
||||
# --primary-bundle-id
|
||||
# - specifying the CFBundleID of the artifact being notarized
|
||||
[[ -n "${NOTARIZATION_TOOL}" ]] || die "NOTARIZATION_TOOL unset"
|
||||
|
||||
# ARTIFACTS_DIR is a required environment variable pointing at a directory to
|
||||
@@ -92,7 +90,7 @@ for ARTIFACT in "${INPUT_SYSX}" "${INPUT_APP}"; do
|
||||
|
||||
echo "notarizing ${BN}"
|
||||
PBID=$(/usr/bin/defaults read "${ARTIFACT}/Contents/Info.plist" CFBundleIdentifier)
|
||||
"${NOTARIZATION_TOOL}" --file "${SCRATCH}/${BN}.zip" --primary-bundle-id "${PBID}"
|
||||
"${NOTARIZATION_TOOL}" --file "${SCRATCH}/${BN}.zip"
|
||||
done
|
||||
|
||||
# Staple the App.
|
||||
@@ -166,8 +164,7 @@ echo "verifying pkg signature"
|
||||
/usr/sbin/pkgutil --check-signature "${SCRATCH}/${RELEASE_NAME}/${RELEASE_NAME}.pkg" || die "bad pkg signature"
|
||||
|
||||
echo "notarizing pkg"
|
||||
"${NOTARIZATION_TOOL}" --file "${SCRATCH}/${RELEASE_NAME}/${RELEASE_NAME}.pkg" \
|
||||
--primary-bundle-id "com.google.santa"
|
||||
"${NOTARIZATION_TOOL}" --file "${SCRATCH}/${RELEASE_NAME}/${RELEASE_NAME}.pkg"
|
||||
|
||||
echo "stapling pkg"
|
||||
/usr/bin/xcrun stapler staple "${SCRATCH}/${RELEASE_NAME}/${RELEASE_NAME}.pkg" || die "failed to staple pkg"
|
||||
@@ -179,7 +176,7 @@ echo "wrapping pkg in dmg"
|
||||
-srcfolder "${SCRATCH}/${RELEASE_NAME}/" "${DMG_PATH}" || die "failed to wrap pkg in dmg"
|
||||
|
||||
echo "notarizing dmg"
|
||||
"${NOTARIZATION_TOOL}" --file "${DMG_PATH}" --primary-bundle-id "com.google.santa"
|
||||
"${NOTARIZATION_TOOL}" --file "${DMG_PATH}"
|
||||
|
||||
echo "stapling dmg"
|
||||
/usr/bin/xcrun stapler staple "${DMG_PATH}" || die "failed to staple dmg"
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
# Santa [](https://github.com/google/santa/actions/workflows/ci.yml)
|
||||
# Santa
|
||||
|
||||
[](https://github.com/google/santa/blob/main/LICENSE)
|
||||
[](https://github.com/google/santa/actions/workflows/ci.yml)
|
||||
[](https://github.com/google/santa/releases/latest)
|
||||
[](https://github.com/google/santa/releases/latest)
|
||||
[](https://github.com/google/santa/releases/latest)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/google/santa/main/Source/gui/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
|
||||
|
||||
14
SECURITY.md
14
SECURITY.md
@@ -1,12 +1,14 @@
|
||||
# Reporting a Vulnerability
|
||||
|
||||
If you believe you have found a security vulnerability, we would appreciate private disclosure
|
||||
so that we can work on a fix before disclosure. Any vulnerabilities reported to us will be
|
||||
If you believe you have found a security vulnerability, we would appreciate a private report
|
||||
so that we can work on and release a fix before public disclosure. Any vulnerabilities reported to us will be
|
||||
disclosed publicly either when a new version with fixes is released or 90 days has passed,
|
||||
whichever comes first.
|
||||
|
||||
To report vulnerabilities to us privately, please e-mail `santa-team@google.com`.
|
||||
If you want to encrypt your e-mail, you can use our GPG key `0x92AFE41DAB49BBB6`
|
||||
available on keyserver.ubuntu.com:
|
||||
To report vulnerabilities to us privately, either:
|
||||
|
||||
`gpg --keyserver keyserver.ubuntu.com --recv-key 0x92AFE41DAB49BBB6`
|
||||
1) Report the vulnerability [through GitHub](https://github.com/google/santa/security/advisories/new).
|
||||
|
||||
2) E-mail `santa-team@google.com`. If you want to encrypt your e-mail, you can use our GPG key `0x92AFE41DAB49BBB6` available on keyserver.ubuntu.com:
|
||||
|
||||
`gpg --keyserver keyserver.ubuntu.com --recv-key 0x92AFE41DAB49BBB6`
|
||||
|
||||
@@ -69,6 +69,11 @@ objc_library(
|
||||
hdrs = ["Platform.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "String",
|
||||
hdrs = ["String.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SantaVnodeHash",
|
||||
srcs = ["SantaVnodeHash.mm"],
|
||||
@@ -128,6 +133,19 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTFileAccessEvent",
|
||||
srcs = ["SNTFileAccessEvent.m"],
|
||||
hdrs = ["SNTFileAccessEvent.h"],
|
||||
module_name = "santa_common_SNTFileAccessEvent",
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
],
|
||||
deps = [
|
||||
"@MOLCertificate",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTCommonEnums",
|
||||
textual_hdrs = ["SNTCommonEnums.h"],
|
||||
|
||||
@@ -191,12 +191,12 @@ using santa::common::PrefixTree;
|
||||
uint32_t count = 4096;
|
||||
auto t = new PrefixTree<int>(count * (uint32_t)[NSUUID UUID].UUIDString.length);
|
||||
|
||||
NSMutableArray *UUIDs = [NSMutableArray arrayWithCapacity:count];
|
||||
__block NSMutableArray *UUIDs = [NSMutableArray arrayWithCapacity:count];
|
||||
for (int i = 0; i < count; ++i) {
|
||||
[UUIDs addObject:[NSUUID UUID].UUIDString];
|
||||
}
|
||||
|
||||
__block BOOL stop = NO;
|
||||
__block _Atomic BOOL stop = NO;
|
||||
|
||||
// Create a bunch of background noise.
|
||||
dispatch_async(dispatch_get_global_queue(0, 0), ^{
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
@property SantaVnode vnodeId;
|
||||
@property SNTEventState decision;
|
||||
@property SNTClientMode decisionClientMode;
|
||||
@property NSString *decisionExtra;
|
||||
@property NSString *sha256;
|
||||
|
||||
@@ -36,6 +37,7 @@
|
||||
@property NSString *certCommonName;
|
||||
@property NSArray<MOLCertificate *> *certChain;
|
||||
@property NSString *teamID;
|
||||
@property NSString *signingID;
|
||||
|
||||
@property NSString *quarantineURL;
|
||||
|
||||
|
||||
@@ -36,12 +36,20 @@ typedef NS_ENUM(NSInteger, SNTAction) {
|
||||
#define RESPONSE_VALID(x) \
|
||||
(x == SNTActionRespondAllow || x == SNTActionRespondDeny || x == SNTActionRespondAllowCompiler)
|
||||
|
||||
// Supported Rule Types
|
||||
//
|
||||
// Note: These enum values should be in order of decreasing precedence as
|
||||
// evaluated by Santa. When adding new enum values, leave some space so that
|
||||
// additional rules can be added without violating this. The ordering isn't
|
||||
// strictly necessary but improves readability and may preemptively prevent
|
||||
// issues should SQLite behavior change.
|
||||
typedef NS_ENUM(NSInteger, SNTRuleType) {
|
||||
SNTRuleTypeUnknown,
|
||||
SNTRuleTypeUnknown = 0,
|
||||
|
||||
SNTRuleTypeBinary = 1,
|
||||
SNTRuleTypeCertificate = 2,
|
||||
SNTRuleTypeTeamID = 3,
|
||||
SNTRuleTypeBinary = 1000,
|
||||
SNTRuleTypeSigningID = 2000,
|
||||
SNTRuleTypeCertificate = 3000,
|
||||
SNTRuleTypeTeamID = 4000,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTRuleState) {
|
||||
@@ -63,32 +71,34 @@ typedef NS_ENUM(NSInteger, SNTClientMode) {
|
||||
SNTClientModeLockdown = 2,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTEventState) {
|
||||
typedef NS_ENUM(uint64_t, SNTEventState) {
|
||||
// Bits 0-15 bits store non-decision types
|
||||
SNTEventStateUnknown = 0,
|
||||
SNTEventStateBundleBinary = 1,
|
||||
|
||||
// Bits 16-23 store deny decision types
|
||||
SNTEventStateBlockUnknown = 1 << 16,
|
||||
SNTEventStateBlockBinary = 1 << 17,
|
||||
SNTEventStateBlockCertificate = 1 << 18,
|
||||
SNTEventStateBlockScope = 1 << 19,
|
||||
SNTEventStateBlockTeamID = 1 << 20,
|
||||
SNTEventStateBlockLongPath = 1 << 21,
|
||||
// Bits 16-39 store deny decision types
|
||||
SNTEventStateBlockUnknown = 1ULL << 16,
|
||||
SNTEventStateBlockBinary = 1ULL << 17,
|
||||
SNTEventStateBlockCertificate = 1ULL << 18,
|
||||
SNTEventStateBlockScope = 1ULL << 19,
|
||||
SNTEventStateBlockTeamID = 1ULL << 20,
|
||||
SNTEventStateBlockLongPath = 1ULL << 21,
|
||||
SNTEventStateBlockSigningID = 1ULL << 22,
|
||||
|
||||
// Bits 24-31 store allow decision types
|
||||
SNTEventStateAllowUnknown = 1 << 24,
|
||||
SNTEventStateAllowBinary = 1 << 25,
|
||||
SNTEventStateAllowCertificate = 1 << 26,
|
||||
SNTEventStateAllowScope = 1 << 27,
|
||||
SNTEventStateAllowCompiler = 1 << 28,
|
||||
SNTEventStateAllowTransitive = 1 << 29,
|
||||
SNTEventStateAllowPendingTransitive = 1 << 30,
|
||||
SNTEventStateAllowTeamID = 1 << 31,
|
||||
// Bits 40-63 store allow decision types
|
||||
SNTEventStateAllowUnknown = 1ULL << 40,
|
||||
SNTEventStateAllowBinary = 1ULL << 41,
|
||||
SNTEventStateAllowCertificate = 1ULL << 42,
|
||||
SNTEventStateAllowScope = 1ULL << 43,
|
||||
SNTEventStateAllowCompiler = 1ULL << 44,
|
||||
SNTEventStateAllowTransitive = 1ULL << 45,
|
||||
SNTEventStateAllowPendingTransitive = 1ULL << 46,
|
||||
SNTEventStateAllowTeamID = 1ULL << 47,
|
||||
SNTEventStateAllowSigningID = 1ULL << 48,
|
||||
|
||||
// Block and Allow masks
|
||||
SNTEventStateBlock = 0xFF << 16,
|
||||
SNTEventStateAllow = 0xFF << 24
|
||||
SNTEventStateBlock = 0xFFFFFFULL << 16,
|
||||
SNTEventStateAllow = 0xFFFFFFULL << 40,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTRuleTableError) {
|
||||
@@ -111,6 +121,7 @@ typedef NS_ENUM(NSInteger, SNTEventLogType) {
|
||||
SNTEventLogTypeSyslog,
|
||||
SNTEventLogTypeFilelog,
|
||||
SNTEventLogTypeProtobuf,
|
||||
SNTEventLogTypeJSON,
|
||||
SNTEventLogTypeNull,
|
||||
};
|
||||
|
||||
@@ -129,6 +140,12 @@ typedef NS_ENUM(NSInteger, SNTSyncStatusType) {
|
||||
SNTSyncStatusTypeUnknown,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTSyncContentEncoding) {
|
||||
SNTSyncContentEncodingNone,
|
||||
SNTSyncContentEncodingDeflate,
|
||||
SNTSyncContentEncodingGzip,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTMetricFormatType) {
|
||||
SNTMetricFormatTypeUnknown,
|
||||
SNTMetricFormatTypeRawJSON,
|
||||
|
||||
@@ -245,10 +245,20 @@
|
||||
///
|
||||
@property(readonly, nonatomic) float spoolDirectoryEventMaxFlushTimeSec;
|
||||
|
||||
///
|
||||
/// If set, contains the filesystem access policy configuration.
|
||||
///
|
||||
/// @note: The property fileAccessPolicyPlist will be ignored if
|
||||
/// fileAccessPolicy is set.
|
||||
/// @note: This property is KVO compliant.
|
||||
///
|
||||
@property(readonly, nonatomic) NSDictionary *fileAccessPolicy;
|
||||
|
||||
///
|
||||
/// If set, contains the path to the filesystem access policy config plist.
|
||||
///
|
||||
/// @note: This property is KVO compliant, but is only read once at santad startup.
|
||||
/// @note: This property will be ignored if fileAccessPolicy is set.
|
||||
/// @note: This property is KVO compliant.
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *fileAccessPolicyPlist;
|
||||
|
||||
@@ -281,6 +291,14 @@
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableSilentMode;
|
||||
|
||||
///
|
||||
/// When silent TTY mode is enabled, Santa will not emit TTY notifications for
|
||||
/// blocked processes.
|
||||
///
|
||||
/// Defaults to NO.
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableSilentTTYMode;
|
||||
|
||||
///
|
||||
/// The text to display when opening Santa.app.
|
||||
/// If unset, the default text will be displayed.
|
||||
@@ -392,6 +410,8 @@
|
||||
///
|
||||
@property(nonatomic) BOOL syncCleanRequired;
|
||||
|
||||
#pragma mark - USB Settings
|
||||
|
||||
///
|
||||
/// USB Mount Blocking. Defaults to false.
|
||||
///
|
||||
@@ -502,6 +522,12 @@
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableBackwardsCompatibleContentEncoding;
|
||||
|
||||
///
|
||||
/// If set, "santactl sync" will use the supplied "Content-Encoding", possible
|
||||
/// settings include "gzip", "deflate", "none". If empty defaults to "deflate".
|
||||
///
|
||||
@property(readonly, nonatomic) SNTSyncContentEncoding syncClientContentEncoding;
|
||||
|
||||
///
|
||||
/// Contains the FCM project name.
|
||||
///
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
@@ -72,6 +73,7 @@ static NSString *const kMachineIDPlistFileKey = @"MachineIDPlist";
|
||||
static NSString *const kMachineIDPlistKeyKey = @"MachineIDKey";
|
||||
|
||||
static NSString *const kEnableSilentModeKey = @"EnableSilentMode";
|
||||
static NSString *const kEnableSilentTTYModeKey = @"EnableSilentTTYMode";
|
||||
static NSString *const kAboutTextKey = @"AboutText";
|
||||
static NSString *const kMoreInfoURLKey = @"MoreInfoURL";
|
||||
static NSString *const kEventDetailURLKey = @"EventDetailURL";
|
||||
@@ -97,6 +99,7 @@ static NSString *const kSpoolDirectoryFileSizeThresholdKB = @"SpoolDirectoryFile
|
||||
static NSString *const kSpoolDirectorySizeThresholdMB = @"SpoolDirectorySizeThresholdMB";
|
||||
static NSString *const kSpoolDirectoryEventMaxFlushTimeSec = @"SpoolDirectoryEventMaxFlushTimeSec";
|
||||
|
||||
static NSString *const kFileAccessPolicy = @"FileAccessPolicy";
|
||||
static NSString *const kFileAccessPolicyPlist = @"FileAccessPolicyPlist";
|
||||
static NSString *const kFileAccessPolicyUpdateIntervalSec = @"FileAccessPolicyUpdateIntervalSec";
|
||||
|
||||
@@ -106,8 +109,7 @@ static NSString *const kEnableForkAndExitLogging = @"EnableForkAndExitLogging";
|
||||
static NSString *const kIgnoreOtherEndpointSecurityClients = @"IgnoreOtherEndpointSecurityClients";
|
||||
static NSString *const kEnableDebugLogging = @"EnableDebugLogging";
|
||||
|
||||
static NSString *const kEnableBackwardsCompatibleContentEncoding =
|
||||
@"EnableBackwardsCompatibleContentEncoding";
|
||||
static NSString *const kClientContentEncoding = @"SyncClientContentEncoding";
|
||||
|
||||
static NSString *const kFCMProject = @"FCMProject";
|
||||
static NSString *const kFCMEntity = @"FCMEntity";
|
||||
@@ -127,7 +129,6 @@ static NSString *const kBlockedPathRegexKeyDeprecated = @"BlacklistRegex";
|
||||
static NSString *const kEnableAllEventUploadKey = @"EnableAllEventUpload";
|
||||
static NSString *const kDisableUnknownEventUploadKey = @"DisableUnknownEventUpload";
|
||||
|
||||
// TODO(markowsky): move these to sync server only.
|
||||
static NSString *const kMetricFormat = @"MetricFormat";
|
||||
static NSString *const kMetricURL = @"MetricURL";
|
||||
static NSString *const kMetricExportInterval = @"MetricExportInterval";
|
||||
@@ -180,6 +181,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kEnablePageZeroProtectionKey : number,
|
||||
kEnableBadSignatureProtectionKey : number,
|
||||
kEnableSilentModeKey : number,
|
||||
kEnableSilentTTYModeKey : number,
|
||||
kAboutTextKey : string,
|
||||
kMoreInfoURLKey : string,
|
||||
kEventDetailURLKey : string,
|
||||
@@ -192,11 +194,13 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kModeNotificationLockdown : string,
|
||||
kStaticRules : array,
|
||||
kSyncBaseURLKey : string,
|
||||
kSyncEnableCleanSyncEventUpload : number,
|
||||
kSyncProxyConfigKey : dictionary,
|
||||
kClientAuthCertificateFileKey : string,
|
||||
kClientAuthCertificatePasswordKey : string,
|
||||
kClientAuthCertificateCNKey : string,
|
||||
kClientAuthCertificateIssuerKey : string,
|
||||
kClientContentEncoding : string,
|
||||
kServerAuthRootsDataKey : data,
|
||||
kServerAuthRootsFileKey : string,
|
||||
kMachineOwnerKey : string,
|
||||
@@ -211,13 +215,13 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kSpoolDirectoryFileSizeThresholdKB : number,
|
||||
kSpoolDirectorySizeThresholdMB : number,
|
||||
kSpoolDirectoryEventMaxFlushTimeSec : number,
|
||||
kFileAccessPolicy : dictionary,
|
||||
kFileAccessPolicyPlist : string,
|
||||
kFileAccessPolicyUpdateIntervalSec : number,
|
||||
kEnableMachineIDDecoration : number,
|
||||
kEnableForkAndExitLogging : number,
|
||||
kIgnoreOtherEndpointSecurityClients : number,
|
||||
kEnableDebugLogging : number,
|
||||
kEnableBackwardsCompatibleContentEncoding : number,
|
||||
kFCMProject : string,
|
||||
kFCMEntity : string,
|
||||
kFCMAPIKey : string,
|
||||
@@ -311,6 +315,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableCleanSyncEventUpload {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnablePageZeroProtection {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -419,6 +427,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicy {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicyPlist {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -455,10 +467,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableBackwardsCompatibleContentEncoding {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFcmProject {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -639,6 +647,11 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (BOOL)enableSilentTTYMode {
|
||||
NSNumber *number = self.configState[kEnableSilentTTYModeKey];
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (NSString *)aboutText {
|
||||
return self.configState[kAboutTextKey];
|
||||
}
|
||||
@@ -702,6 +715,20 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return self.configState[kClientAuthCertificateIssuerKey];
|
||||
}
|
||||
|
||||
- (SNTSyncContentEncoding)syncClientContentEncoding {
|
||||
NSString *contentEncoding = [self.configState[kClientContentEncoding] lowercaseString];
|
||||
if ([contentEncoding isEqualToString:@"deflate"]) {
|
||||
return SNTSyncContentEncodingDeflate;
|
||||
} else if ([contentEncoding isEqualToString:@"gzip"]) {
|
||||
return SNTSyncContentEncodingGzip;
|
||||
} else if ([contentEncoding isEqualToString:@"none"]) {
|
||||
return SNTSyncContentEncodingNone;
|
||||
} else {
|
||||
// Ensure we have the same default zlib behavior Santa's always had otherwise.
|
||||
return SNTSyncContentEncodingDeflate;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSData *)syncServerAuthRootsData {
|
||||
return self.configState[kServerAuthRootsDataKey];
|
||||
}
|
||||
@@ -772,6 +799,8 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return SNTEventLogTypeSyslog;
|
||||
} else if ([logType isEqualToString:@"null"]) {
|
||||
return SNTEventLogTypeNull;
|
||||
} else if ([logType isEqualToString:@"json"]) {
|
||||
return SNTEventLogTypeJSON;
|
||||
} else if ([logType isEqualToString:@"file"]) {
|
||||
return SNTEventLogTypeFilelog;
|
||||
} else {
|
||||
@@ -809,8 +838,17 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
: 15.0;
|
||||
}
|
||||
|
||||
- (NSDictionary *)fileAccessPolicy {
|
||||
return self.configState[kFileAccessPolicy];
|
||||
}
|
||||
|
||||
- (NSString *)fileAccessPolicyPlist {
|
||||
return self.configState[kFileAccessPolicyPlist];
|
||||
// This property is ignored when kFileAccessPolicy is set
|
||||
if (self.configState[kFileAccessPolicy]) {
|
||||
return nil;
|
||||
} else {
|
||||
return self.configState[kFileAccessPolicyPlist];
|
||||
}
|
||||
}
|
||||
|
||||
- (uint32_t)fileAccessPolicyUpdateIntervalSec {
|
||||
@@ -866,11 +904,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [number boolValue] || self.debugFlag;
|
||||
}
|
||||
|
||||
- (BOOL)enableBackwardsCompatibleContentEncoding {
|
||||
NSNumber *number = self.configState[kEnableBackwardsCompatibleContentEncoding];
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (NSString *)fcmProject {
|
||||
return self.configState[kFCMProject];
|
||||
}
|
||||
@@ -1120,6 +1153,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
for (id rule in staticRules) {
|
||||
if (![rule isKindOfClass:[NSDictionary class]]) return;
|
||||
SNTRule *r = [[SNTRule alloc] initWithDictionary:rule];
|
||||
if (!r) continue;
|
||||
rules[r.identifier] = r;
|
||||
}
|
||||
self.cachedStaticRules = [rules copy];
|
||||
|
||||
83
Source/common/SNTFileAccessEvent.h
Normal file
83
Source/common/SNTFileAccessEvent.h
Normal file
@@ -0,0 +1,83 @@
|
||||
/// 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;
|
||||
|
||||
// TODO(mlw): Store signing chain info
|
||||
// @property NSArray<MOLCertificate*> *signingChain;
|
||||
|
||||
@end
|
||||
79
Source/common/SNTFileAccessEvent.m
Normal file
79
Source/common/SNTFileAccessEvent.m
Normal file
@@ -0,0 +1,79 @@
|
||||
/// 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"
|
||||
|
||||
@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)
|
||||
|
||||
- (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);
|
||||
}
|
||||
|
||||
- (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);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString
|
||||
stringWithFormat:@"SNTFileAccessEvent: Accessed: %@, By: %@", self.accessedPath, self.filePath];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -33,6 +33,15 @@
|
||||
_type = type;
|
||||
_customMsg = customMsg;
|
||||
_timestamp = timestamp;
|
||||
|
||||
if (_type == SNTRuleTypeBinary || _type == SNTRuleTypeCertificate) {
|
||||
NSCharacterSet *nonHex =
|
||||
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
|
||||
if ([[_identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length != 64)
|
||||
return nil;
|
||||
} else if (_identifier.length == 0) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -55,52 +64,54 @@
|
||||
- (instancetype)initWithDictionary:(NSDictionary *)dict {
|
||||
if (![dict isKindOfClass:[NSDictionary class]]) return nil;
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_identifier = dict[kRuleIdentifier];
|
||||
if (![_identifier isKindOfClass:[NSString class]] || !_identifier.length) {
|
||||
_identifier = dict[kRuleSHA256];
|
||||
}
|
||||
if (![_identifier isKindOfClass:[NSString class]] || !_identifier.length) return nil;
|
||||
|
||||
NSString *policyString = dict[kRulePolicy];
|
||||
if (![policyString isKindOfClass:[NSString class]]) return nil;
|
||||
if ([policyString isEqual:kRulePolicyAllowlist] ||
|
||||
[policyString isEqual:kRulePolicyAllowlistDeprecated]) {
|
||||
_state = SNTRuleStateAllow;
|
||||
} else if ([policyString isEqual:kRulePolicyAllowlistCompiler] ||
|
||||
[policyString isEqual:kRulePolicyAllowlistCompilerDeprecated]) {
|
||||
_state = SNTRuleStateAllowCompiler;
|
||||
} else if ([policyString isEqual:kRulePolicyBlocklist] ||
|
||||
[policyString isEqual:kRulePolicyBlocklistDeprecated]) {
|
||||
_state = SNTRuleStateBlock;
|
||||
} else if ([policyString isEqual:kRulePolicySilentBlocklist] ||
|
||||
[policyString isEqual:kRulePolicySilentBlocklistDeprecated]) {
|
||||
_state = SNTRuleStateSilentBlock;
|
||||
} else if ([policyString isEqual:kRulePolicyRemove]) {
|
||||
_state = SNTRuleStateRemove;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *ruleTypeString = dict[kRuleType];
|
||||
if (![ruleTypeString isKindOfClass:[NSString class]]) return nil;
|
||||
if ([ruleTypeString isEqual:kRuleTypeBinary]) {
|
||||
_type = SNTRuleTypeBinary;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeCertificate]) {
|
||||
_type = SNTRuleTypeCertificate;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeTeamID]) {
|
||||
_type = SNTRuleTypeTeamID;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *customMsg = dict[kRuleCustomMsg];
|
||||
if ([customMsg isKindOfClass:[NSString class]] && customMsg.length) {
|
||||
_customMsg = customMsg;
|
||||
}
|
||||
NSString *identifier = dict[kRuleIdentifier];
|
||||
if (![identifier isKindOfClass:[NSString class]] || !identifier.length) {
|
||||
identifier = dict[kRuleSHA256];
|
||||
}
|
||||
return self;
|
||||
if (![identifier isKindOfClass:[NSString class]] || !identifier.length) return nil;
|
||||
|
||||
NSString *policyString = dict[kRulePolicy];
|
||||
SNTRuleState state;
|
||||
if (![policyString isKindOfClass:[NSString class]]) return nil;
|
||||
if ([policyString isEqual:kRulePolicyAllowlist] ||
|
||||
[policyString isEqual:kRulePolicyAllowlistDeprecated]) {
|
||||
state = SNTRuleStateAllow;
|
||||
} else if ([policyString isEqual:kRulePolicyAllowlistCompiler] ||
|
||||
[policyString isEqual:kRulePolicyAllowlistCompilerDeprecated]) {
|
||||
state = SNTRuleStateAllowCompiler;
|
||||
} else if ([policyString isEqual:kRulePolicyBlocklist] ||
|
||||
[policyString isEqual:kRulePolicyBlocklistDeprecated]) {
|
||||
state = SNTRuleStateBlock;
|
||||
} else if ([policyString isEqual:kRulePolicySilentBlocklist] ||
|
||||
[policyString isEqual:kRulePolicySilentBlocklistDeprecated]) {
|
||||
state = SNTRuleStateSilentBlock;
|
||||
} else if ([policyString isEqual:kRulePolicyRemove]) {
|
||||
state = SNTRuleStateRemove;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *ruleTypeString = dict[kRuleType];
|
||||
SNTRuleType type;
|
||||
if (![ruleTypeString isKindOfClass:[NSString class]]) return nil;
|
||||
if ([ruleTypeString isEqual:kRuleTypeBinary]) {
|
||||
type = SNTRuleTypeBinary;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeCertificate]) {
|
||||
type = SNTRuleTypeCertificate;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeTeamID]) {
|
||||
type = SNTRuleTypeTeamID;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeSigningID]) {
|
||||
type = SNTRuleTypeSigningID;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *customMsg = dict[kRuleCustomMsg];
|
||||
if (![customMsg isKindOfClass:[NSString class]] || customMsg.length == 0) {
|
||||
customMsg = nil;
|
||||
}
|
||||
|
||||
return [self initWithIdentifier:identifier state:state type:type customMsg:customMsg];
|
||||
}
|
||||
|
||||
#pragma mark NSSecureCoding
|
||||
|
||||
@@ -25,22 +25,24 @@
|
||||
SNTRule *sut;
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"some-sort-of-identifier",
|
||||
@"identifier" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
@"policy" : @"ALLOWLIST",
|
||||
@"rule_type" : @"BINARY",
|
||||
}];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
|
||||
XCTAssertEqualObjects(sut.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(sut.type, SNTRuleTypeBinary);
|
||||
XCTAssertEqual(sut.state, SNTRuleStateAllow);
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"sha256" : @"some-sort-of-identifier",
|
||||
@"sha256" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
@"policy" : @"BLOCKLIST",
|
||||
@"rule_type" : @"CERTIFICATE",
|
||||
}];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
|
||||
XCTAssertEqualObjects(sut.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(sut.type, SNTRuleTypeCertificate);
|
||||
XCTAssertEqual(sut.state, SNTRuleStateBlock);
|
||||
|
||||
@@ -55,12 +57,13 @@
|
||||
XCTAssertEqual(sut.state, SNTRuleStateSilentBlock);
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"some-sort-of-identifier",
|
||||
@"identifier" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
@"policy" : @"ALLOWLIST_COMPILER",
|
||||
@"rule_type" : @"BINARY",
|
||||
}];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
|
||||
XCTAssertEqualObjects(sut.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(sut.type, SNTRuleTypeBinary);
|
||||
XCTAssertEqual(sut.state, SNTRuleStateAllowCompiler);
|
||||
|
||||
@@ -94,12 +97,19 @@
|
||||
XCTAssertNil(sut);
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"an-identifier",
|
||||
@"identifier" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
}];
|
||||
XCTAssertNil(sut);
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"an-identifier",
|
||||
@"policy" : @"ALLOWLIST",
|
||||
@"rule_type" : @"BINARY",
|
||||
}];
|
||||
XCTAssertNil(sut);
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@"identifier" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
@"policy" : @"OTHERPOLICY",
|
||||
@"rule_type" : @"BINARY",
|
||||
}];
|
||||
|
||||
@@ -100,6 +100,11 @@
|
||||
///
|
||||
@property NSString *teamID;
|
||||
|
||||
///
|
||||
/// If the executed file was signed, this is the Signing ID if present in the signature information.
|
||||
///
|
||||
@property NSString *signingID;
|
||||
|
||||
///
|
||||
/// The user who executed the binary.
|
||||
///
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
|
||||
ENCODE(self.signingChain, @"signingChain");
|
||||
ENCODE(self.teamID, @"teamID");
|
||||
ENCODE(self.signingID, @"signingID");
|
||||
|
||||
ENCODE(self.executingUser, @"executingUser");
|
||||
ENCODE(self.occurrenceDate, @"occurrenceDate");
|
||||
@@ -95,10 +96,11 @@
|
||||
|
||||
_signingChain = DECODEARRAY(MOLCertificate, @"signingChain");
|
||||
_teamID = DECODE(NSString, @"teamID");
|
||||
_signingID = DECODE(NSString, @"signingID");
|
||||
|
||||
_executingUser = DECODE(NSString, @"executingUser");
|
||||
_occurrenceDate = DECODE(NSDate, @"occurrenceDate");
|
||||
_decision = (SNTEventState)[DECODE(NSNumber, @"decision") intValue];
|
||||
_decision = (SNTEventState)[DECODE(NSNumber, @"decision") unsignedLongLongValue];
|
||||
_pid = DECODE(NSNumber, @"pid");
|
||||
_ppid = DECODE(NSNumber, @"ppid");
|
||||
_parentName = DECODE(NSString, @"parentName");
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
extern NSString *const kXSRFToken;
|
||||
extern NSString *const kDefaultXSRFTokenHeader;
|
||||
extern NSString *const kXSRFTokenHeader;
|
||||
|
||||
extern NSString *const kSerialNumber;
|
||||
extern NSString *const kHostname;
|
||||
@@ -41,6 +42,7 @@ extern NSString *const kCertificateRuleCount;
|
||||
extern NSString *const kCompilerRuleCount;
|
||||
extern NSString *const kTransitiveRuleCount;
|
||||
extern NSString *const kTeamIDRuleCount;
|
||||
extern NSString *const kSigningIDRuleCount;
|
||||
extern NSString *const kFullSyncInterval;
|
||||
extern NSString *const kFCMToken;
|
||||
extern NSString *const kFCMFullSyncInterval;
|
||||
@@ -65,11 +67,13 @@ extern NSString *const kDecisionAllowBinary;
|
||||
extern NSString *const kDecisionAllowCertificate;
|
||||
extern NSString *const kDecisionAllowScope;
|
||||
extern NSString *const kDecisionAllowTeamID;
|
||||
extern NSString *const kDecisionAllowSigningID;
|
||||
extern NSString *const kDecisionBlockUnknown;
|
||||
extern NSString *const kDecisionBlockBinary;
|
||||
extern NSString *const kDecisionBlockCertificate;
|
||||
extern NSString *const kDecisionBlockScope;
|
||||
extern NSString *const kDecisionBlockTeamID;
|
||||
extern NSString *const kDecisionBlockSigningID;
|
||||
extern NSString *const kDecisionUnknown;
|
||||
extern NSString *const kDecisionBundleBinary;
|
||||
extern NSString *const kLoggedInUsers;
|
||||
@@ -94,6 +98,7 @@ extern NSString *const kCertOU;
|
||||
extern NSString *const kCertValidFrom;
|
||||
extern NSString *const kCertValidUntil;
|
||||
extern NSString *const kTeamID;
|
||||
extern NSString *const kSigningID;
|
||||
extern NSString *const kQuarantineDataURL;
|
||||
extern NSString *const kQuarantineRefererURL;
|
||||
extern NSString *const kQuarantineTimestamp;
|
||||
@@ -117,6 +122,7 @@ extern NSString *const kRuleType;
|
||||
extern NSString *const kRuleTypeBinary;
|
||||
extern NSString *const kRuleTypeCertificate;
|
||||
extern NSString *const kRuleTypeTeamID;
|
||||
extern NSString *const kRuleTypeSigningID;
|
||||
extern NSString *const kRuleCustomMsg;
|
||||
extern NSString *const kCursor;
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
|
||||
#import "Source/common/SNTSyncConstants.h"
|
||||
|
||||
NSString *const kXSRFToken = @"X-XSRF-TOKEN";
|
||||
NSString *const kDefaultXSRFTokenHeader = @"X-XSRF-TOKEN";
|
||||
NSString *const kXSRFTokenHeader = @"X-XSRF-TOKEN-HEADER";
|
||||
|
||||
NSString *const kSerialNumber = @"serial_num";
|
||||
NSString *const kHostname = @"hostname";
|
||||
@@ -41,6 +42,7 @@ NSString *const kCertificateRuleCount = @"certificate_rule_count";
|
||||
NSString *const kCompilerRuleCount = @"compiler_rule_count";
|
||||
NSString *const kTransitiveRuleCount = @"transitive_rule_count";
|
||||
NSString *const kTeamIDRuleCount = @"teamid_rule_count";
|
||||
NSString *const kSigningIDRuleCount = @"signingid_rule_count";
|
||||
NSString *const kFullSyncInterval = @"full_sync_interval";
|
||||
NSString *const kFCMToken = @"fcm_token";
|
||||
NSString *const kFCMFullSyncInterval = @"fcm_full_sync_interval";
|
||||
@@ -66,11 +68,13 @@ NSString *const kDecisionAllowBinary = @"ALLOW_BINARY";
|
||||
NSString *const kDecisionAllowCertificate = @"ALLOW_CERTIFICATE";
|
||||
NSString *const kDecisionAllowScope = @"ALLOW_SCOPE";
|
||||
NSString *const kDecisionAllowTeamID = @"ALLOW_TEAMID";
|
||||
NSString *const kDecisionAllowSigningID = @"ALLOW_SIGNINGID";
|
||||
NSString *const kDecisionBlockUnknown = @"BLOCK_UNKNOWN";
|
||||
NSString *const kDecisionBlockBinary = @"BLOCK_BINARY";
|
||||
NSString *const kDecisionBlockCertificate = @"BLOCK_CERTIFICATE";
|
||||
NSString *const kDecisionBlockScope = @"BLOCK_SCOPE";
|
||||
NSString *const kDecisionBlockTeamID = @"BLOCK_TEAMID";
|
||||
NSString *const kDecisionBlockSigningID = @"BLOCK_SIGNINGID";
|
||||
NSString *const kDecisionUnknown = @"UNKNOWN";
|
||||
NSString *const kDecisionBundleBinary = @"BUNDLE_BINARY";
|
||||
NSString *const kLoggedInUsers = @"logged_in_users";
|
||||
@@ -95,6 +99,7 @@ NSString *const kCertOU = @"ou";
|
||||
NSString *const kCertValidFrom = @"valid_from";
|
||||
NSString *const kCertValidUntil = @"valid_until";
|
||||
NSString *const kTeamID = @"team_id";
|
||||
NSString *const kSigningID = @"signing_id";
|
||||
NSString *const kQuarantineDataURL = @"quarantine_data_url";
|
||||
NSString *const kQuarantineRefererURL = @"quarantine_referer_url";
|
||||
NSString *const kQuarantineTimestamp = @"quarantine_timestamp";
|
||||
@@ -118,6 +123,7 @@ NSString *const kRuleType = @"rule_type";
|
||||
NSString *const kRuleTypeBinary = @"BINARY";
|
||||
NSString *const kRuleTypeCertificate = @"CERTIFICATE";
|
||||
NSString *const kRuleTypeTeamID = @"TEAMID";
|
||||
NSString *const kRuleTypeSigningID = @"SIGNINGID";
|
||||
NSString *const kRuleCustomMsg = @"custom_msg";
|
||||
NSString *const kCursor = @"cursor";
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
- (void)databaseRuleForBinarySHA256:(NSString *)binarySHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
reply:(void (^)(SNTRule *))reply;
|
||||
|
||||
///
|
||||
|
||||
@@ -17,13 +17,16 @@
|
||||
#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)postUSBBlockNotification:(SNTDeviceEvent *)event withCustomMessage:(NSString *)message;
|
||||
- (void)postFileAccessBlockNotification:(SNTFileAccessEvent *)event
|
||||
withCustomMessage:(NSString *)message API_AVAILABLE(macos(13.0));
|
||||
- (void)postClientModeNotification:(SNTClientMode)clientmode;
|
||||
- (void)postRuleSyncNotificationWithCustomMessage:(NSString *)message;
|
||||
- (void)updateCountsForEvent:(SNTStoredEvent *)event
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
/// Database ops
|
||||
///
|
||||
- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive, int64_t teamID))reply;
|
||||
int64_t transitive, int64_t teamID, int64_t signingID))reply;
|
||||
- (void)databaseEventCount:(void (^)(int64_t count))reply;
|
||||
- (void)staticRuleCount:(void (^)(int64_t count))reply;
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
fileSHA256:(NSString *)fileSHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
reply:(void (^)(SNTEventState))reply;
|
||||
|
||||
///
|
||||
|
||||
43
Source/common/String.h
Normal file
43
Source/common/String.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.
|
||||
|
||||
#ifndef SANTA__COMMON__STRING_H
|
||||
#define SANTA__COMMON__STRING_H
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace santa::common {
|
||||
|
||||
static inline std::string_view NSStringToUTF8StringView(NSString *str) {
|
||||
return std::string_view(str.UTF8String, [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
|
||||
}
|
||||
|
||||
static inline std::string NSStringToUTF8String(NSString *str) {
|
||||
return std::string(str.UTF8String, [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
|
||||
}
|
||||
|
||||
static inline NSString *StringToNSString(const std::string &str) {
|
||||
return [NSString stringWithUTF8String:str.c_str()];
|
||||
}
|
||||
|
||||
static inline NSString *StringToNSString(const char *str) {
|
||||
return [NSString stringWithUTF8String:str];
|
||||
}
|
||||
|
||||
} // namespace santa::common
|
||||
|
||||
#endif
|
||||
@@ -35,6 +35,9 @@ uint64_t MachTimeToNanos(uint64_t mach_time);
|
||||
// Convert nanoseconds to mach absolute time
|
||||
uint64_t NanosToMachTime(uint64_t nanos);
|
||||
|
||||
// Add some number of nanoseconds to a given mach time and return the new result
|
||||
uint64_t AddNanosecondsToMachTime(uint64_t ns, uint64_t machTime);
|
||||
|
||||
// Get the result of proc_pidinfo with the PROC_PIDTASKINFO flavor
|
||||
std::optional<SantaTaskInfo> GetTaskInfo();
|
||||
|
||||
|
||||
@@ -39,17 +39,28 @@ static mach_timebase_info_data_t GetTimebase() {
|
||||
}
|
||||
|
||||
uint64_t MachTimeToNanos(uint64_t mach_time) {
|
||||
mach_timebase_info_data_t timebase = GetTimebase();
|
||||
static mach_timebase_info_data_t timebase = GetTimebase();
|
||||
|
||||
return mach_time * timebase.numer / timebase.denom;
|
||||
}
|
||||
|
||||
uint64_t NanosToMachTime(uint64_t nanos) {
|
||||
mach_timebase_info_data_t timebase = GetTimebase();
|
||||
static mach_timebase_info_data_t timebase = GetTimebase();
|
||||
|
||||
return nanos * timebase.denom / timebase.numer;
|
||||
}
|
||||
|
||||
uint64_t AddNanosecondsToMachTime(uint64_t ns, uint64_t machTime) {
|
||||
// Convert machtime to nanoseconds
|
||||
uint64_t nanoTime = MachTimeToNanos(machTime);
|
||||
|
||||
// Add the nanosecond offset
|
||||
nanoTime += ns;
|
||||
|
||||
// Convert back to machTime
|
||||
return NanosToMachTime(nanoTime);
|
||||
}
|
||||
|
||||
std::optional<SantaTaskInfo> GetTaskInfo() {
|
||||
struct proc_taskinfo pti;
|
||||
|
||||
|
||||
@@ -38,9 +38,12 @@
|
||||
// Pretty print C++ string match errors
|
||||
#define XCTAssertCppStringEqual(got, want) XCTAssertCStringEqual((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.
|
||||
|
||||
@@ -87,16 +87,6 @@ es_process_t MakeESProcess(es_file_t *file, audit_token_t tok, audit_token_t par
|
||||
};
|
||||
}
|
||||
|
||||
static uint64_t AddMillisToMachTime(uint64_t ms, uint64_t machTime) {
|
||||
uint64_t nanoTime = MachTimeToNanos(machTime);
|
||||
|
||||
// Add the ms offset
|
||||
nanoTime += (ms * NSEC_PER_MSEC);
|
||||
|
||||
// Convert back to machTime
|
||||
return NanosToMachTime(nanoTime);
|
||||
}
|
||||
|
||||
uint32_t MaxSupportedESMessageVersionForCurrentOS() {
|
||||
// Note: ES message v3 was only in betas.
|
||||
if (@available(macOS 13.0, *)) {
|
||||
@@ -115,7 +105,7 @@ uint32_t MaxSupportedESMessageVersionForCurrentOS() {
|
||||
es_message_t MakeESMessage(es_event_type_t et, es_process_t *proc, ActionType action_type,
|
||||
uint64_t future_deadline_ms) {
|
||||
es_message_t es_msg = {
|
||||
.deadline = AddMillisToMachTime(future_deadline_ms, mach_absolute_time()),
|
||||
.deadline = AddNanosecondsToMachTime(future_deadline_ms * NSEC_PER_MSEC, mach_absolute_time()),
|
||||
.process = proc,
|
||||
.action_type =
|
||||
(action_type == ActionType::Notify) ? ES_ACTION_TYPE_NOTIFY : ES_ACTION_TYPE_AUTH,
|
||||
|
||||
@@ -231,10 +231,10 @@ message Execution {
|
||||
optional FileInfo working_directory = 4;
|
||||
|
||||
// List of process arguments
|
||||
repeated string args = 5;
|
||||
repeated bytes args = 5;
|
||||
|
||||
// List of environment variables
|
||||
repeated string envs = 6;
|
||||
repeated bytes envs = 6;
|
||||
|
||||
// List of file descriptors
|
||||
repeated FileDescriptor fds = 7;
|
||||
@@ -262,6 +262,7 @@ message Execution {
|
||||
REASON_TRANSITIVE = 8;
|
||||
REASON_LONG_PATH = 9;
|
||||
REASON_NOT_RUNNING = 10;
|
||||
REASON_SIGNING_ID = 11;
|
||||
}
|
||||
optional Reason reason = 10;
|
||||
|
||||
|
||||
@@ -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,11 @@ objc_library(
|
||||
deps = [
|
||||
":SNTAboutWindowView",
|
||||
":SNTDeviceMessageWindowView",
|
||||
":SNTFileAccessMessageWindowView",
|
||||
"//Source/common:SNTBlockMessage_SantaGUI",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDeviceEvent",
|
||||
"//Source/common:SNTFileAccessEvent",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SNTStrengthify",
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import santa_common_SNTConfigurator
|
||||
@@ -37,7 +51,7 @@ struct SNTDeviceMessageWindowView: View {
|
||||
Text("Device Name").bold()
|
||||
Text("Device BSD Path").bold()
|
||||
|
||||
if event!.remountArgs.count > 0 {
|
||||
if event!.remountArgs?.count ?? 0 > 0 {
|
||||
Text("Remount Mode").bold()
|
||||
}
|
||||
}
|
||||
@@ -46,7 +60,7 @@ struct SNTDeviceMessageWindowView: View {
|
||||
Text(event!.mntonname)
|
||||
Text(event!.mntfromname)
|
||||
|
||||
if event!.remountArgs.count > 0 {
|
||||
if event!.remountArgs?.count ?? 0 > 0 {
|
||||
Text(event!.readableRemountArgs())
|
||||
}
|
||||
}
|
||||
|
||||
35
Source/gui/SNTFileAccessMessageWindowController.h
Normal file
35
Source/gui/SNTFileAccessMessageWindowController.h
Normal file
@@ -0,0 +1,35 @@
|
||||
/// 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 message:(nullable NSString *)message;
|
||||
|
||||
@property(readonly) SNTFileAccessEvent *event;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
79
Source/gui/SNTFileAccessMessageWindowController.m
Normal file
79
Source/gui/SNTFileAccessMessageWindowController.m
Normal file
@@ -0,0 +1,79 @@
|
||||
/// 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 SNTFileAccessEvent *event;
|
||||
@end
|
||||
|
||||
@implementation SNTFileAccessMessageWindowController
|
||||
|
||||
- (instancetype)initWithEvent:(SNTFileAccessEvent *)event message:(nullable NSString *)message {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_customMessage = message;
|
||||
_event = event;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)showWindow:(id)sender {
|
||||
if (self.window) {
|
||||
[self.window orderOut:sender];
|
||||
}
|
||||
|
||||
self.window =
|
||||
[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 0, 0)
|
||||
styleMask:NSWindowStyleMaskClosable | NSWindowStyleMaskTitled
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
|
||||
self.window.contentViewController =
|
||||
[SNTFileAccessMessageWindowViewFactory createWithWindow:self.window
|
||||
event:self.event
|
||||
customMsg:self.attributedCustomMessage];
|
||||
|
||||
self.window.delegate = self;
|
||||
|
||||
// Add app to Cmd+Tab and Dock.
|
||||
NSApp.activationPolicy = NSApplicationActivationPolicyRegular;
|
||||
|
||||
[super showWindow:sender];
|
||||
}
|
||||
|
||||
- (void)windowWillClose:(NSNotification *)notification {
|
||||
// Remove app from Cmd+Tab and Dock.
|
||||
NSApp.activationPolicy = NSApplicationActivationPolicyAccessory;
|
||||
[super windowWillClose:notification];
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedCustomMessage {
|
||||
return [SNTBlockMessage formatMessage:self.customMessage];
|
||||
}
|
||||
|
||||
- (NSString *)messageHash {
|
||||
// TODO(mlw): This is not the final form. As this feature is expanded this
|
||||
// hash will need to be revisted to ensure it meets our needs.
|
||||
return [NSString stringWithFormat:@"%@|%@|%d", self.event.ruleName, self.event.ruleVersion,
|
||||
[self.event.pid intValue]];
|
||||
}
|
||||
|
||||
@end
|
||||
158
Source/gui/SNTFileAccessMessageWindowView.swift
Normal file
158
Source/gui/SNTFileAccessMessageWindowView.swift
Normal file
@@ -0,0 +1,158 @@
|
||||
/// 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_SNTFileAccessEvent
|
||||
|
||||
@available(macOS 13, *)
|
||||
@objc public class SNTFileAccessMessageWindowViewFactory : NSObject {
|
||||
@objc public static func createWith(window: NSWindow, event: SNTFileAccessEvent, customMsg: NSAttributedString?) -> NSViewController {
|
||||
return NSHostingController(rootView:SNTFileAccessMessageWindowView(window:window, event:event, customMsg:customMsg)
|
||||
.frame(width:800, height:600))
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13, *)
|
||||
struct Property : View {
|
||||
var lbl: String
|
||||
var val: String
|
||||
|
||||
var body: some View {
|
||||
let width: CGFloat? = 150
|
||||
|
||||
HStack(spacing: 5) {
|
||||
Text(lbl + ":")
|
||||
.frame(width: width, alignment: .trailing)
|
||||
.lineLimit(1)
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.padding(Edge.Set.horizontal, 10)
|
||||
|
||||
Text(val)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13, *)
|
||||
struct Event: View {
|
||||
let e: SNTFileAccessEvent
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 customMsg: NSAttributedString?
|
||||
|
||||
@State private 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 = customMsg {
|
||||
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!)
|
||||
|
||||
Toggle(isOn: $checked) {
|
||||
Text("Prevent future notifications for this application for a day")
|
||||
.font(Font.system(size: 11.0));
|
||||
}
|
||||
|
||||
VStack(spacing:15) {
|
||||
Button(action: openButton, label: {
|
||||
Text("Open Event Info...").frame(maxWidth:.infinity)
|
||||
})
|
||||
Button(action: dismissButton, label: {
|
||||
Text("Dismiss").frame(maxWidth:.infinity)
|
||||
})
|
||||
.keyboardShortcut(.return)
|
||||
}.frame(width: 220)
|
||||
|
||||
Spacer()
|
||||
|
||||
}.frame(maxWidth:800.0).fixedSize()
|
||||
}
|
||||
|
||||
func publisherInfo() {
|
||||
// TODO(mlw): Will hook up in a separate PR
|
||||
print("showing publisher popup...")
|
||||
}
|
||||
|
||||
func openButton() {
|
||||
// TODO(mlw): Will hook up in a separate PR
|
||||
print("opening event info...")
|
||||
}
|
||||
|
||||
func dismissButton() {
|
||||
window?.close()
|
||||
print("close window")
|
||||
}
|
||||
}
|
||||
|
||||
@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(), customMsg: 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 ()
|
||||
@@ -342,6 +345,19 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
|
||||
[self queueMessage:pendingMsg];
|
||||
}
|
||||
|
||||
- (void)postFileAccessBlockNotification:(SNTFileAccessEvent *)event
|
||||
withCustomMessage:(NSString *)message API_AVAILABLE(macos(13.0)) {
|
||||
if (!event) {
|
||||
LOGI(@"Error: Missing event object in message received from daemon!");
|
||||
return;
|
||||
}
|
||||
|
||||
SNTFileAccessMessageWindowController *pendingMsg =
|
||||
[[SNTFileAccessMessageWindowController alloc] initWithEvent:event message:message];
|
||||
|
||||
[self queueMessage:pendingMsg];
|
||||
}
|
||||
|
||||
#pragma mark SNTBundleNotifierXPC protocol methods
|
||||
|
||||
- (void)updateCountsForEvent:(SNTStoredEvent *)event
|
||||
|
||||
@@ -37,14 +37,14 @@ objc_library(
|
||||
objc_library(
|
||||
name = "santactl_lib",
|
||||
srcs = [
|
||||
"main.m",
|
||||
"Commands/SNTCommandFileInfo.m",
|
||||
"Commands/SNTCommandRule.m",
|
||||
"Commands/SNTCommandStatus.m",
|
||||
"Commands/SNTCommandVersion.m",
|
||||
"Commands/SNTCommandMetrics.h",
|
||||
"Commands/SNTCommandMetrics.m",
|
||||
"Commands/SNTCommandRule.m",
|
||||
"Commands/SNTCommandStatus.m",
|
||||
"Commands/SNTCommandSync.m",
|
||||
"Commands/SNTCommandVersion.m",
|
||||
"main.m",
|
||||
] + select({
|
||||
"//:opt_build": [],
|
||||
"//conditions:default": [
|
||||
|
||||
@@ -42,6 +42,7 @@ static NSString *const kRule = @"Rule";
|
||||
static NSString *const kSigningChain = @"Signing Chain";
|
||||
static NSString *const kUniversalSigningChain = @"Universal Signing Chain";
|
||||
static NSString *const kTeamID = @"Team ID";
|
||||
static NSString *const kSigningID = @"Signing ID";
|
||||
|
||||
// signing chain keys
|
||||
static NSString *const kCommonName = @"Common Name";
|
||||
@@ -111,6 +112,7 @@ typedef id (^SNTAttributeBlock)(SNTCommandFileInfo *, SNTFileInfo *);
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock downloadTimestamp;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock downloadAgent;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock teamID;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock signingID;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock type;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock pageZero;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock codeSigned;
|
||||
@@ -184,8 +186,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
+ (NSArray<NSString *> *)fileInfoKeys {
|
||||
return @[
|
||||
kPath, kSHA256, kSHA1, kBundleName, kBundleVersion, kBundleVersionStr, kDownloadReferrerURL,
|
||||
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kType, kPageZero, kCodeSigned, kRule,
|
||||
kSigningChain, kUniversalSigningChain
|
||||
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kSigningID, kType, kPageZero,
|
||||
kCodeSigned, kRule, kSigningChain, kUniversalSigningChain
|
||||
];
|
||||
}
|
||||
|
||||
@@ -218,6 +220,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
kSigningChain : self.signingChain,
|
||||
kUniversalSigningChain : self.universalSigningChain,
|
||||
kTeamID : self.teamID,
|
||||
kSigningID : self.signingID,
|
||||
};
|
||||
|
||||
_printQueue = dispatch_queue_create("com.google.santactl.print_queue", DISPATCH_QUEUE_SERIAL);
|
||||
@@ -357,15 +360,34 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
NSError *err;
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:&err];
|
||||
[[cmd.daemonConn remoteObjectProxy]
|
||||
decisionForFilePath:fileInfo.path
|
||||
fileSHA256:fileInfo.SHA256
|
||||
certificateSHA256:err ? nil : csc.leafCertificate.SHA256
|
||||
teamID:[csc.signingInformation valueForKey:@"teamid"]
|
||||
reply:^(SNTEventState s) {
|
||||
state = s;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
|
||||
NSString *teamID =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier];
|
||||
NSString *identifier =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
|
||||
|
||||
NSString *signingID;
|
||||
if (identifier) {
|
||||
if (teamID) {
|
||||
signingID = [NSString stringWithFormat:@"%@:%@", teamID, identifier];
|
||||
} else {
|
||||
id platformID =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoPlatformIdentifier];
|
||||
if ([platformID isKindOfClass:[NSNumber class]] && [platformID intValue] != 0) {
|
||||
signingID = [NSString stringWithFormat:@"platform:%@", identifier];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[[cmd.daemonConn remoteObjectProxy] decisionForFilePath:fileInfo.path
|
||||
fileSHA256:fileInfo.SHA256
|
||||
certificateSHA256:err ? nil : csc.leafCertificate.SHA256
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
reply:^(SNTEventState s) {
|
||||
state = s;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
|
||||
cmd.daemonUnavailable = YES;
|
||||
return kCommunicationErrorMsg;
|
||||
@@ -381,6 +403,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
case SNTEventStateBlockCertificate: [output appendString:@" (Certificate)"]; break;
|
||||
case SNTEventStateAllowTeamID:
|
||||
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
|
||||
case SNTEventStateAllowSigningID:
|
||||
case SNTEventStateBlockSigningID: [output appendString:@" (SigningID)"]; break;
|
||||
case SNTEventStateAllowScope:
|
||||
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
|
||||
case SNTEventStateAllowCompiler: [output appendString:@" (Compiler)"]; break;
|
||||
@@ -473,6 +497,13 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
};
|
||||
}
|
||||
|
||||
- (SNTAttributeBlock)signingID {
|
||||
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
|
||||
return [csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
// Entry point for the command.
|
||||
|
||||
@@ -60,16 +60,28 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
@" Will add the hash of the file currently at that path.\n"
|
||||
@" Does not work with --check. Use the fileinfo verb to check.\n"
|
||||
@" the rule state of a file.\n"
|
||||
@" --identifier {sha256|teamID}: identifier to add/remove/check\n"
|
||||
@" --identifier {sha256|teamID|signingID}: identifier to add/remove/check\n"
|
||||
@" --sha256 {sha256}: hash to add/remove/check [deprecated]\n"
|
||||
@"\n"
|
||||
@" Optionally:\n"
|
||||
@" --teamid: add or check a team ID rule instead of binary\n"
|
||||
@" --signingid: add or check a signing ID rule instead of binary (see notes)\n"
|
||||
@" --certificate: add or check a certificate sha256 rule instead of binary\n"
|
||||
#ifdef DEBUG
|
||||
@" --force: allow manual changes even when SyncBaseUrl is set\n"
|
||||
#endif
|
||||
@" --message {message}: custom message\n");
|
||||
@" --message {message}: custom message\n"
|
||||
@"\n"
|
||||
@" Notes:\n"
|
||||
@" The format of `identifier` when adding/checking a `signingid` rule is:\n"
|
||||
@"\n"
|
||||
@" `TeamID:SigningID`\n"
|
||||
@"\n"
|
||||
@" Because signing IDs are controlled by the binary author, this ensures\n"
|
||||
@" that the signing ID is properly scoped to a developer. For the special\n"
|
||||
@" case of platform binaries, `TeamID` should be replaced with the string\n"
|
||||
@" \"platform\" (e.g. `platform:SigningID`). This allows for rules\n"
|
||||
@" targeting Apple-signed binaries that do not have a team ID.\n");
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
@@ -116,6 +128,8 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
newRule.type = SNTRuleTypeCertificate;
|
||||
} else if ([arg caseInsensitiveCompare:@"--teamid"] == NSOrderedSame) {
|
||||
newRule.type = SNTRuleTypeTeamID;
|
||||
} else if ([arg caseInsensitiveCompare:@"--signingid"] == NSOrderedSame) {
|
||||
newRule.type = SNTRuleTypeSigningID;
|
||||
} else if ([arg caseInsensitiveCompare:@"--path"] == NSOrderedSame) {
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"--path requires an argument"];
|
||||
@@ -131,9 +145,6 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
[self printErrorUsageAndExit:@"--sha256 requires an argument"];
|
||||
}
|
||||
newRule.identifier = arguments[i];
|
||||
if (newRule.identifier.length != 64) {
|
||||
[self printErrorUsageAndExit:@"--sha256 requires a valid SHA-256 as the argument"];
|
||||
}
|
||||
} else if ([arg caseInsensitiveCompare:@"--message"] == NSOrderedSame) {
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"--message requires an argument"];
|
||||
@@ -148,11 +159,6 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
}
|
||||
}
|
||||
|
||||
if (check) {
|
||||
if (!newRule.identifier) return [self printErrorUsageAndExit:@"--check requires --identifier"];
|
||||
return [self printStateOfRule:newRule daemonConnection:self.daemonConn];
|
||||
}
|
||||
|
||||
if (path) {
|
||||
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithPath:path];
|
||||
if (!fi.path) {
|
||||
@@ -164,10 +170,25 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
} else if (newRule.type == SNTRuleTypeCertificate) {
|
||||
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
|
||||
newRule.identifier = cs.leafCertificate.SHA256;
|
||||
} else if (newRule.type == SNTRuleTypeTeamID) {
|
||||
} else if (newRule.type == SNTRuleTypeTeamID || newRule.type == SNTRuleTypeSigningID) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
if (newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate) {
|
||||
NSCharacterSet *nonHex =
|
||||
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
|
||||
if ([[newRule.identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length !=
|
||||
64) {
|
||||
[self printErrorUsageAndExit:@"BINARY or CERTIFICATE rules require a valid SHA-256"];
|
||||
}
|
||||
}
|
||||
|
||||
if (check) {
|
||||
if (!newRule.identifier) return [self printErrorUsageAndExit:@"--check requires --identifier"];
|
||||
return [self printStateOfRule:newRule daemonConnection:self.daemonConn];
|
||||
}
|
||||
|
||||
if (newRule.state == SNTRuleStateUnknown) {
|
||||
[self printErrorUsageAndExit:@"No state specified"];
|
||||
} else if (!newRule.identifier) {
|
||||
@@ -214,11 +235,13 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
NSString *fileSHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil;
|
||||
NSString *certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil;
|
||||
NSString *teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil;
|
||||
NSString *signingID = (rule.type == SNTRuleTypeSigningID) ? rule.identifier : nil;
|
||||
__block NSMutableString *output;
|
||||
[rop decisionForFilePath:nil
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
reply:^(SNTEventState s) {
|
||||
output =
|
||||
(SNTEventStateAllow & s) ? @"Allowed".mutableCopy : @"Blocked".mutableCopy;
|
||||
@@ -241,6 +264,10 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
break;
|
||||
case SNTEventStateAllowTeamID:
|
||||
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
|
||||
case SNTEventStateAllowSigningID:
|
||||
case SNTEventStateBlockSigningID:
|
||||
[output appendString:@" (SigningID)"];
|
||||
break;
|
||||
default: output = @"None".mutableCopy; break;
|
||||
}
|
||||
if (isatty(STDOUT_FILENO)) {
|
||||
@@ -260,6 +287,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
[rop databaseRuleForBinarySHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
reply:^(SNTRule *r) {
|
||||
if (r.state == SNTRuleStateAllowTransitive) {
|
||||
NSDate *date =
|
||||
|
||||
@@ -81,13 +81,19 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
}];
|
||||
|
||||
// Database counts
|
||||
__block int64_t eventCount = -1, binaryRuleCount = -1, certRuleCount = -1, teamIDRuleCount = -1;
|
||||
__block int64_t compilerRuleCount = -1, transitiveRuleCount = -1;
|
||||
__block int64_t eventCount = -1;
|
||||
__block int64_t binaryRuleCount = -1;
|
||||
__block int64_t certRuleCount = -1;
|
||||
__block int64_t teamIDRuleCount = -1;
|
||||
__block int64_t signingIDRuleCount = -1;
|
||||
__block int64_t compilerRuleCount = -1;
|
||||
__block int64_t transitiveRuleCount = -1;
|
||||
[rop databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive, int64_t teamID) {
|
||||
int64_t transitive, int64_t teamID, int64_t signingID) {
|
||||
binaryRuleCount = binary;
|
||||
certRuleCount = certificate;
|
||||
teamIDRuleCount = teamID;
|
||||
signingIDRuleCount = signingID;
|
||||
compilerRuleCount = compiler;
|
||||
transitiveRuleCount = transitive;
|
||||
}];
|
||||
@@ -193,6 +199,8 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"database" : @{
|
||||
@"binary_rules" : @(binaryRuleCount),
|
||||
@"certificate_rules" : @(certRuleCount),
|
||||
@"teamid_rules" : @(teamIDRuleCount),
|
||||
@"signingid_rules" : @(signingIDRuleCount),
|
||||
@"compiler_rules" : @(compilerRuleCount),
|
||||
@"transitive_rules" : @(transitiveRuleCount),
|
||||
@"events_pending_upload" : @(eventCount),
|
||||
@@ -217,7 +225,7 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"enabled" : @(watchItemsEnabled),
|
||||
@"rule_count" : @(watchItemsRuleCount),
|
||||
@"policy_version" : watchItemsPolicyVersion,
|
||||
@"config_path" : watchItemsConfigPath,
|
||||
@"config_path" : watchItemsConfigPath ?: @"null",
|
||||
@"last_policy_update" : watchItemsLastUpdateStr ?: @"null",
|
||||
};
|
||||
} else {
|
||||
@@ -258,6 +266,7 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
printf(" %-25s | %lld\n", "Binary Rules", binaryRuleCount);
|
||||
printf(" %-25s | %lld\n", "Certificate Rules", certRuleCount);
|
||||
printf(" %-25s | %lld\n", "TeamID Rules", teamIDRuleCount);
|
||||
printf(" %-25s | %lld\n", "SigningID Rules", signingIDRuleCount);
|
||||
printf(" %-25s | %lld\n", "Compiler Rules", compilerRuleCount);
|
||||
printf(" %-25s | %lld\n", "Transitive Rules", transitiveRuleCount);
|
||||
printf(" %-25s | %lld\n", "Events Pending Upload", eventCount);
|
||||
@@ -272,7 +281,7 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
if (watchItemsEnabled) {
|
||||
printf(" %-25s | %s\n", "Policy Version", watchItemsPolicyVersion.UTF8String);
|
||||
printf(" %-25s | %llu\n", "Rule Count", watchItemsRuleCount);
|
||||
printf(" %-25s | %s\n", "Config Path", watchItemsConfigPath.UTF8String);
|
||||
printf(" %-25s | %s\n", "Config Path", (watchItemsConfigPath ?: @"(embedded)").UTF8String);
|
||||
printf(" %-25s | %s\n", "Last Policy Update", watchItemsLastUpdateStr.UTF8String);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ objc_library(
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:String",
|
||||
"//Source/common:Unit",
|
||||
],
|
||||
)
|
||||
@@ -208,6 +209,16 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "TTYWriter",
|
||||
srcs = ["TTYWriter.mm"],
|
||||
hdrs = ["TTYWriter.h"],
|
||||
deps = [
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:String",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTExecutionController",
|
||||
srcs = ["SNTExecutionController.mm"],
|
||||
@@ -220,6 +231,7 @@ objc_library(
|
||||
":SNTPolicyProcessor",
|
||||
":SNTRuleTable",
|
||||
":SNTSyncdQueue",
|
||||
":TTYWriter",
|
||||
"//Source/common:BranchPrediction",
|
||||
"//Source/common:SNTBlockMessage",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
@@ -232,6 +244,7 @@ objc_library(
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:String",
|
||||
"@MOLCodesignChecker",
|
||||
],
|
||||
)
|
||||
@@ -284,7 +297,9 @@ objc_library(
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:String",
|
||||
"//Source/common:Unit",
|
||||
],
|
||||
)
|
||||
@@ -330,6 +345,9 @@ objc_library(
|
||||
name = "SNTEndpointSecurityFileAccessAuthorizer",
|
||||
srcs = ["EventProviders/SNTEndpointSecurityFileAccessAuthorizer.mm"],
|
||||
hdrs = ["EventProviders/SNTEndpointSecurityFileAccessAuthorizer.h"],
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
":EndpointSecurityAPI",
|
||||
":EndpointSecurityEnrichedTypes",
|
||||
@@ -337,6 +355,7 @@ objc_library(
|
||||
":EndpointSecurityLogger",
|
||||
":EndpointSecurityMessage",
|
||||
":Metrics",
|
||||
":RateLimiter",
|
||||
":SNTDecisionCache",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
@@ -345,10 +364,13 @@ objc_library(
|
||||
"//Source/common:Platform",
|
||||
"//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",
|
||||
],
|
||||
@@ -380,12 +402,24 @@ objc_library(
|
||||
":EndpointSecurityClient",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "RateLimiter",
|
||||
srcs = ["EventProviders/RateLimiter.mm"],
|
||||
hdrs = ["EventProviders/RateLimiter.h"],
|
||||
deps = [
|
||||
":Metrics",
|
||||
"//Source/common:BranchPrediction",
|
||||
"//Source/common:SystemResources",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "EndpointSecurityEnricher",
|
||||
srcs = ["EventProviders/EndpointSecurity/Enricher.mm"],
|
||||
@@ -455,6 +489,7 @@ objc_library(
|
||||
],
|
||||
deps = [
|
||||
":EndpointSecurityMessage",
|
||||
"//Source/common:String",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -469,7 +504,6 @@ objc_library(
|
||||
":EndpointSecuritySerializerUtilities",
|
||||
":SNTDecisionCache",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
],
|
||||
@@ -488,6 +522,7 @@ objc_library(
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:String",
|
||||
"//Source/common:santa_cc_proto_library_wrapper",
|
||||
],
|
||||
)
|
||||
@@ -556,6 +591,7 @@ objc_library(
|
||||
":EndpointSecurityWriterNull",
|
||||
":EndpointSecurityWriterSpool",
|
||||
":EndpointSecurityWriterSyslog",
|
||||
":SNTDecisionCache",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
@@ -660,6 +696,7 @@ objc_library(
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTFileAccessEvent",
|
||||
"//Source/common:SNTKVOManager",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTXPCNotifierInterface",
|
||||
@@ -681,11 +718,13 @@ objc_library(
|
||||
":Metrics",
|
||||
":SNTCompilerController",
|
||||
":SNTDatabaseController",
|
||||
":SNTDecisionCache",
|
||||
":SNTEventTable",
|
||||
":SNTExecutionController",
|
||||
":SNTNotificationQueue",
|
||||
":SNTRuleTable",
|
||||
":SNTSyncdQueue",
|
||||
":TTYWriter",
|
||||
":WatchItems",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTConfigurator",
|
||||
@@ -962,6 +1001,16 @@ santa_unit_test(
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "RateLimiterTest",
|
||||
srcs = ["EventProviders/RateLimiterTest.mm"],
|
||||
deps = [
|
||||
":Metrics",
|
||||
":RateLimiter",
|
||||
"//Source/common:SystemResources",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "EndpointSecuritySerializerEmptyTest",
|
||||
srcs = ["Logs/EndpointSecurity/Serializers/EmptyTest.mm"],
|
||||
@@ -1228,6 +1277,7 @@ santa_unit_test(
|
||||
":SNTCompilerController",
|
||||
":SNTEndpointSecurityRecorder",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:TestUtils",
|
||||
"//Source/common:Unit",
|
||||
"@OCMock",
|
||||
@@ -1294,6 +1344,7 @@ test_suite(
|
||||
":EndpointSecurityWriterFileTest",
|
||||
":EndpointSecurityWriterSpoolTest",
|
||||
":MetricsTest",
|
||||
":RateLimiterTest",
|
||||
":SNTApplicationCoreMetricsTest",
|
||||
":SNTCompilerControllerTest",
|
||||
":SNTDecisionCacheTest",
|
||||
|
||||
@@ -39,9 +39,10 @@
|
||||
bail = YES;
|
||||
return;
|
||||
}
|
||||
[db close];
|
||||
[[NSFileManager defaultManager] removeItemAtPath:[db databasePath] error:NULL];
|
||||
[db open];
|
||||
[self closeDeleteReopenDatabase:db];
|
||||
} else if ([db userVersion] > [self currentSupportedVersion]) {
|
||||
LOGW(@"Database version newer than supported. Deleting.");
|
||||
[self closeDeleteReopenDatabase:db];
|
||||
}
|
||||
}];
|
||||
|
||||
@@ -58,11 +59,22 @@
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)closeDeleteReopenDatabase:(FMDatabase *)db {
|
||||
[db close];
|
||||
[[NSFileManager defaultManager] removeItemAtPath:[db databasePath] error:NULL];
|
||||
[db open];
|
||||
}
|
||||
|
||||
- (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (uint32_t)currentSupportedVersion {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Called at the end of initialization to ensure the table in the
|
||||
/// database exists and uses the latest schema.
|
||||
- (void)updateTableSchema {
|
||||
|
||||
@@ -18,8 +18,14 @@
|
||||
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
|
||||
static const uint32_t kEventTableCurrentVersion = 3;
|
||||
|
||||
@implementation SNTEventTable
|
||||
|
||||
- (uint32_t)currentSupportedVersion {
|
||||
return kEventTableCurrentVersion;
|
||||
}
|
||||
|
||||
- (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version {
|
||||
int newVersion = 0;
|
||||
|
||||
|
||||
@@ -57,10 +57,16 @@
|
||||
- (NSUInteger)teamIDRuleCount;
|
||||
|
||||
///
|
||||
/// @return Rule for binary or certificate with given SHA-256. The binary rule will be returned
|
||||
/// if it exists. If not, the certificate rule will be returned if it exists.
|
||||
/// @return Number of signing ID rules in the database
|
||||
///
|
||||
- (NSUInteger)signingIDRuleCount;
|
||||
|
||||
///
|
||||
/// @return Rule for binary, signingID, certificate or teamID (in that order).
|
||||
/// The first matching rule found is returned.
|
||||
///
|
||||
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
|
||||
signingID:(NSString *)signingID
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID;
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
|
||||
static const uint32_t kRuleTableCurrentVersion = 5;
|
||||
|
||||
// TODO(nguyenphillip): this should be configurable.
|
||||
// How many rules must be in database before we start trying to remove transitive rules.
|
||||
static const NSUInteger kTransitiveRuleCullingThreshold = 500000;
|
||||
@@ -173,6 +175,10 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
self.criticalSystemBinaries = bins;
|
||||
}
|
||||
|
||||
- (uint32_t)currentSupportedVersion {
|
||||
return kRuleTableCurrentVersion;
|
||||
}
|
||||
|
||||
- (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version {
|
||||
// Lock this database from other processes
|
||||
[[db executeQuery:@"PRAGMA locking_mode = EXCLUSIVE;"] close];
|
||||
@@ -204,12 +210,25 @@ 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;
|
||||
}
|
||||
|
||||
// Save signing info for launchd and santad. Used to ensure they are always allowed.
|
||||
self.santadCSInfo = [[MOLCodesignChecker alloc] initWithSelf];
|
||||
self.launchdCSInfo = [[MOLCodesignChecker alloc] initWithPID:1];
|
||||
@@ -230,20 +249,20 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSUInteger)binaryRuleCount {
|
||||
- (NSUInteger)ruleCountForRuleType:(SNTRuleType)ruleType {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=1"];
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=?", @(ruleType)];
|
||||
}];
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSUInteger)binaryRuleCount {
|
||||
return [self ruleCountForRuleType:SNTRuleTypeBinary];
|
||||
}
|
||||
|
||||
- (NSUInteger)certificateRuleCount {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=2"];
|
||||
}];
|
||||
return count;
|
||||
return [self ruleCountForRuleType:SNTRuleTypeCertificate];
|
||||
}
|
||||
|
||||
- (NSUInteger)compilerRuleCount {
|
||||
@@ -265,11 +284,11 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
}
|
||||
|
||||
- (NSUInteger)teamIDRuleCount {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=3"];
|
||||
}];
|
||||
return count;
|
||||
return [self ruleCountForRuleType:SNTRuleTypeTeamID];
|
||||
}
|
||||
|
||||
- (NSUInteger)signingIDRuleCount {
|
||||
return [self ruleCountForRuleType:SNTRuleTypeSigningID];
|
||||
}
|
||||
|
||||
- (SNTRule *)ruleFromResultSet:(FMResultSet *)rs {
|
||||
@@ -281,6 +300,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
}
|
||||
|
||||
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
|
||||
signingID:(NSString *)signingID
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID {
|
||||
__block SNTRule *rule;
|
||||
@@ -288,12 +308,27 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
// Look for a static rule that matches.
|
||||
NSDictionary *staticRules = [[SNTConfigurator configurator] staticRules];
|
||||
if (staticRules.count) {
|
||||
// IMPORTANT: The order static rules are checked here should be the same
|
||||
// order as given by the SQL query for the rules database.
|
||||
rule = staticRules[binarySHA256];
|
||||
if (rule.type == SNTRuleTypeBinary) return rule;
|
||||
if (rule.type == SNTRuleTypeBinary) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[signingID];
|
||||
if (rule.type == SNTRuleTypeSigningID) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[certificateSHA256];
|
||||
if (rule.type == SNTRuleTypeCertificate) return rule;
|
||||
if (rule.type == SNTRuleTypeCertificate) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[teamID];
|
||||
if (rule.type == SNTRuleTypeTeamID) return rule;
|
||||
if (rule.type == SNTRuleTypeTeamID) {
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
|
||||
// Now query the database.
|
||||
@@ -301,7 +336,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
// NOTE: This code is written with the intention that the binary rule is searched for first
|
||||
// as Santa is designed to go with the most-specific rule possible.
|
||||
//
|
||||
// The intended order of precedence is Binaries > Certificates > Team IDs.
|
||||
// The intended order of precedence is Binaries > Signing IDs > Certificates > Team IDs.
|
||||
//
|
||||
// As such the query should have "ORDER BY type DESC" before the LIMIT, to ensure that is the
|
||||
// case. However, in all tested versions of SQLite that ORDER BY clause is unnecessary: the query
|
||||
@@ -316,10 +351,12 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
// There is a test for this in SNTRuleTableTests in case SQLite behavior changes in the future.
|
||||
//
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
FMResultSet *rs =
|
||||
[db executeQuery:@"SELECT * FROM rules WHERE (identifier=? and type=1) OR "
|
||||
@"(identifier=? AND type=2) OR (identifier=? AND type=3) LIMIT 1",
|
||||
binarySHA256, certificateSHA256, teamID];
|
||||
FMResultSet *rs = [db executeQuery:@"SELECT * FROM rules WHERE "
|
||||
@" (identifier=? and type=1000) "
|
||||
@"OR (identifier=? AND type=2000) "
|
||||
@"OR (identifier=? AND type=3000) "
|
||||
@"OR (identifier=? AND type=4000) LIMIT 1",
|
||||
binarySHA256, signingID, certificateSHA256, teamID];
|
||||
if ([rs next]) {
|
||||
rule = [self ruleFromResultSet:rs];
|
||||
}
|
||||
|
||||
@@ -43,9 +43,22 @@
|
||||
return r;
|
||||
}
|
||||
|
||||
- (SNTRule *)_exampleSigningIDRuleIsPlatform:(BOOL)isPlatformBinary {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
if (isPlatformBinary) {
|
||||
r.identifier = @"platform:signingID";
|
||||
} else {
|
||||
r.identifier = @"teamID:signingID";
|
||||
}
|
||||
r.state = SNTRuleStateBlock;
|
||||
r.type = SNTRuleTypeSigningID;
|
||||
r.customMsg = @"A teamID rule";
|
||||
return r;
|
||||
}
|
||||
|
||||
- (SNTRule *)_exampleBinaryRule {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
r.identifier = @"a";
|
||||
r.identifier = @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670";
|
||||
r.state = SNTRuleStateBlock;
|
||||
r.type = SNTRuleTypeBinary;
|
||||
r.customMsg = @"A rule";
|
||||
@@ -54,7 +67,7 @@
|
||||
|
||||
- (SNTRule *)_exampleCertRule {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
r.identifier = @"b";
|
||||
r.identifier = @"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258";
|
||||
r.state = SNTRuleStateAllow;
|
||||
r.type = SNTRuleTypeCertificate;
|
||||
return r;
|
||||
@@ -112,7 +125,7 @@
|
||||
|
||||
- (void)testAddInvalidRule {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
r.identifier = @"a";
|
||||
r.identifier = @"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258";
|
||||
r.type = SNTRuleTypeCertificate;
|
||||
|
||||
NSError *error;
|
||||
@@ -125,12 +138,21 @@
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil];
|
||||
SNTRule *r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"a");
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeBinary);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"b" certificateSHA256:nil teamID:nil];
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"b6ee1c3c5a715c049d14a8457faa6b6701b8507efe908300e238e0768bd759c2"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
@@ -139,12 +161,21 @@
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil certificateSHA256:@"b" teamID:nil];
|
||||
SNTRule *r = [self.sut
|
||||
ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:nil];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"b");
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCertificate);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil certificateSHA256:@"a" teamID:nil];
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:@"5bdab1288fc16892fef50c658db54f1e2e19cf8f71cc55f77de2b95e051e2562"
|
||||
teamID:nil];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
@@ -153,38 +184,108 @@
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil certificateSHA256:nil teamID:@"teamID"];
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeTeamID);
|
||||
XCTAssertEqual([self.sut teamIDRuleCount], 1);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil certificateSHA256:nil teamID:@"nonexistentTeamID"];
|
||||
r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@"nonexistentTeamID"];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
- (void)testFetchSigningIDRule {
|
||||
[self.sut addRules:@[
|
||||
[self _exampleBinaryRule], [self _exampleSigningIDRuleIsPlatform:YES],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO]
|
||||
]
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
|
||||
XCTAssertEqual([self.sut signingIDRuleCount], 2);
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:@"teamID:signingID"
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:@"platform:signingID"
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"platform:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil signingID:@"nonexistent" certificateSHA256:nil teamID:nil];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
- (void)testFetchRuleOrdering {
|
||||
[self.sut
|
||||
addRules:@[ [self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule] ]
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
[self.sut addRules:@[
|
||||
[self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO]
|
||||
]
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
|
||||
// This test verifies that the implicit rule ordering we've been abusing is still working.
|
||||
// See the comment in SNTRuleTable#ruleForBinarySHA256:certificateSHA256:teamID
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:@"a" certificateSHA256:@"b" teamID:@"teamID"];
|
||||
SNTRule *r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:@"teamID:signingID"
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"a");
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeBinary, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"a" certificateSHA256:@"unknowncert" teamID:@"teamID"];
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:@"teamID:signingID"
|
||||
certificateSHA256:@"unknowncert"
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"a");
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeBinary, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"unknown" certificateSHA256:@"b" teamID:@"teamID"];
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"unknown"
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"b");
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCertificate, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"teamID:signingID"
|
||||
certificateSHA256:@"unknown"
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID, @"Implicit rule ordering failed (SigningID)");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"unknown"
|
||||
certificateSHA256:@"unknown"
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeTeamID, @"Implicit rule ordering failed (TeamID)");
|
||||
}
|
||||
|
||||
- (void)testBadDatabase {
|
||||
|
||||
@@ -33,6 +33,7 @@ static constexpr WatchItemPathType kWatchItemPolicyDefaultPathType =
|
||||
WatchItemPathType::kLiteral;
|
||||
static constexpr bool kWatchItemPolicyDefaultAllowReadAccess = false;
|
||||
static constexpr bool kWatchItemPolicyDefaultAuditOnly = true;
|
||||
static constexpr bool kWatchItemPolicyDefaultInvertProcessExceptions = false;
|
||||
|
||||
struct WatchItemPolicy {
|
||||
struct Process {
|
||||
@@ -69,19 +70,23 @@ struct WatchItemPolicy {
|
||||
WatchItemPathType pt = kWatchItemPolicyDefaultPathType,
|
||||
bool ara = kWatchItemPolicyDefaultAllowReadAccess,
|
||||
bool ao = kWatchItemPolicyDefaultAuditOnly,
|
||||
bool ipe = kWatchItemPolicyDefaultInvertProcessExceptions,
|
||||
std::vector<Process> procs = {})
|
||||
: name(n),
|
||||
path(p),
|
||||
path_type(pt),
|
||||
allow_read_access(ara),
|
||||
audit_only(ao),
|
||||
invert_process_exceptions(ipe),
|
||||
processes(std::move(procs)) {}
|
||||
|
||||
bool operator==(const WatchItemPolicy &other) const {
|
||||
return name == other.name && path == other.path &&
|
||||
path_type == other.path_type &&
|
||||
allow_read_access == other.allow_read_access &&
|
||||
audit_only == other.audit_only && processes == other.processes;
|
||||
audit_only == other.audit_only &&
|
||||
invert_process_exceptions == other.invert_process_exceptions &&
|
||||
processes == other.processes;
|
||||
}
|
||||
|
||||
bool operator!=(const WatchItemPolicy &other) const {
|
||||
@@ -93,7 +98,12 @@ struct WatchItemPolicy {
|
||||
WatchItemPathType path_type;
|
||||
bool allow_read_access;
|
||||
bool audit_only;
|
||||
bool invert_process_exceptions;
|
||||
std::vector<Process> processes;
|
||||
|
||||
// WIP - No current way to control via config
|
||||
bool silent = true;
|
||||
std::string version = "temp_version";
|
||||
};
|
||||
|
||||
} // namespace santa::santad::data_layer
|
||||
|
||||
@@ -39,6 +39,7 @@ extern NSString *const kWatchItemConfigKeyPathsIsPrefix;
|
||||
extern NSString *const kWatchItemConfigKeyOptions;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsAllowReadAccess;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsAuditOnly;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsInvertProcessExceptions;
|
||||
extern NSString *const kWatchItemConfigKeyProcesses;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesBinaryPath;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesCertificateSha256;
|
||||
@@ -70,9 +71,15 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
|
||||
// Factory
|
||||
static std::shared_ptr<WatchItems> Create(NSString *config_path,
|
||||
uint64_t reapply_config_frequency_secs);
|
||||
// Factory
|
||||
static std::shared_ptr<WatchItems> Create(NSDictionary *config,
|
||||
uint64_t reapply_config_frequency_secs);
|
||||
|
||||
WatchItems(NSString *config_path_, dispatch_queue_t q, dispatch_source_t timer_source,
|
||||
WatchItems(NSString *config_path, dispatch_queue_t q, dispatch_source_t timer_source,
|
||||
void (^periodic_task_complete_f)(void) = nullptr);
|
||||
WatchItems(NSDictionary *config, dispatch_queue_t q, dispatch_source_t timer_source,
|
||||
void (^periodic_task_complete_f)(void) = nullptr);
|
||||
|
||||
~WatchItems();
|
||||
|
||||
void BeginPeriodicTask();
|
||||
@@ -80,6 +87,8 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
|
||||
void RegisterClient(id<SNTEndpointSecurityDynamicEventHandler> client);
|
||||
|
||||
void SetConfigPath(NSString *config_path);
|
||||
void SetConfig(NSDictionary *config);
|
||||
|
||||
VersionAndPolicies FindPolciesForPaths(const std::vector<std::string_view> &paths);
|
||||
|
||||
std::optional<WatchItemsState> State();
|
||||
@@ -87,6 +96,9 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
|
||||
friend class santa::santad::data_layer::WatchItemsPeer;
|
||||
|
||||
private:
|
||||
static std::shared_ptr<WatchItems> CreateInternal(NSString *config_path, NSDictionary *config,
|
||||
uint64_t reapply_config_frequency_secs);
|
||||
|
||||
NSDictionary *ReadConfig();
|
||||
NSDictionary *ReadConfigLocked() ABSL_SHARED_LOCKS_REQUIRED(lock_);
|
||||
void ReloadConfig(NSDictionary *new_config);
|
||||
@@ -98,6 +110,7 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
|
||||
std::set<std::pair<std::string, WatchItemPathType>> &paths);
|
||||
|
||||
NSString *config_path_;
|
||||
NSDictionary *embedded_config_;
|
||||
dispatch_queue_t q_;
|
||||
dispatch_source_t timer_source_;
|
||||
void (^periodic_task_complete_f_)(void);
|
||||
|
||||
@@ -34,8 +34,12 @@
|
||||
|
||||
#import "Source/common/PrefixTree.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/String.h"
|
||||
#import "Source/common/Unit.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
|
||||
using santa::common::NSStringToUTF8String;
|
||||
using santa::common::NSStringToUTF8StringView;
|
||||
using santa::common::PrefixTree;
|
||||
using santa::common::Unit;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
@@ -49,6 +53,7 @@ NSString *const kWatchItemConfigKeyPathsIsPrefix = @"IsPrefix";
|
||||
NSString *const kWatchItemConfigKeyOptions = @"Options";
|
||||
NSString *const kWatchItemConfigKeyOptionsAllowReadAccess = @"AllowReadAccess";
|
||||
NSString *const kWatchItemConfigKeyOptionsAuditOnly = @"AuditOnly";
|
||||
NSString *const kWatchItemConfigKeyOptionsInvertProcessExceptions = @"InvertProcessExceptions";
|
||||
NSString *const kWatchItemConfigKeyProcesses = @"Processes";
|
||||
NSString *const kWatchItemConfigKeyProcessesBinaryPath = @"BinaryPath";
|
||||
NSString *const kWatchItemConfigKeyProcessesCertificateSha256 = @"CertificateSha256";
|
||||
@@ -255,7 +260,7 @@ std::variant<Unit, PathList> VerifyConfigWatchItemPaths(NSArray<id> *paths, NSEr
|
||||
return Unit{};
|
||||
}
|
||||
|
||||
path_list.push_back({std::string(path_str.UTF8String, path_str.length), path_type});
|
||||
path_list.push_back({NSStringToUTF8String(path_str), path_type});
|
||||
} else if ([path isKindOfClass:[NSString class]]) {
|
||||
if (!LenRangeValidator(1, PATH_MAX)(path, err)) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Invalid path length: %@",
|
||||
@@ -264,8 +269,8 @@ std::variant<Unit, PathList> VerifyConfigWatchItemPaths(NSArray<id> *paths, NSEr
|
||||
return Unit{};
|
||||
}
|
||||
|
||||
path_list.push_back({std::string(((NSString *)path).UTF8String, ((NSString *)path).length),
|
||||
kWatchItemPolicyDefaultPathType});
|
||||
path_list.push_back(
|
||||
{NSStringToUTF8String(((NSString *)path)), kWatchItemPolicyDefaultPathType});
|
||||
} else {
|
||||
PopulateError(
|
||||
err, [NSString stringWithFormat:
|
||||
@@ -340,12 +345,11 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
|
||||
}
|
||||
|
||||
proc_list.push_back(WatchItemPolicy::Process(
|
||||
std::string([(process[kWatchItemConfigKeyProcessesBinaryPath] ?: @"") UTF8String]),
|
||||
std::string([(process[kWatchItemConfigKeyProcessesSigningID] ?: @"") UTF8String]),
|
||||
std::string([(process[kWatchItemConfigKeyProcessesTeamID] ?: @"") UTF8String]),
|
||||
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesBinaryPath] ?: @""),
|
||||
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesSigningID] ?: @""),
|
||||
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesTeamID] ?: @""),
|
||||
HexStringToBytes(process[kWatchItemConfigKeyProcessesCDHash]),
|
||||
std::string(
|
||||
[(process[kWatchItemConfigKeyProcessesCertificateSha256] ?: @"") UTF8String]),
|
||||
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesCertificateSha256] ?: @""),
|
||||
process[kWatchItemConfigKeyProcessesPlatformBinary]
|
||||
? std::make_optional(
|
||||
(bool)[process[kWatchItemConfigKeyProcessesPlatformBinary] boolValue])
|
||||
@@ -373,6 +377,8 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
|
||||
/// <false/>
|
||||
/// <key>AuditOnly</key>
|
||||
/// <false/>
|
||||
/// <key>InvertProcessExceptions</key>
|
||||
/// <false/>
|
||||
/// </dict>
|
||||
/// <key>Processes</key>
|
||||
/// <array>
|
||||
@@ -407,6 +413,11 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsAuditOnly, [NSNumber class], err)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsInvertProcessExceptions,
|
||||
[NSNumber class], err)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool allow_read_access = options[kWatchItemConfigKeyOptionsAllowReadAccess]
|
||||
@@ -415,6 +426,10 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
bool audit_only = options[kWatchItemConfigKeyOptionsAuditOnly]
|
||||
? [options[kWatchItemConfigKeyOptionsAuditOnly] boolValue]
|
||||
: kWatchItemPolicyDefaultAuditOnly;
|
||||
bool invert_process_exceptions =
|
||||
options[kWatchItemConfigKeyOptionsInvertProcessExceptions]
|
||||
? [options[kWatchItemConfigKeyOptionsInvertProcessExceptions] boolValue]
|
||||
: kWatchItemPolicyDefaultInvertProcessExceptions;
|
||||
|
||||
std::variant<Unit, ProcessList> proc_list = VerifyConfigWatchItemProcesses(watch_item, err);
|
||||
if (std::holds_alternative<Unit>(proc_list)) {
|
||||
@@ -423,8 +438,8 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
|
||||
for (const PathAndTypePair &path_type_pair : std::get<PathList>(path_list)) {
|
||||
policies.push_back(std::make_shared<WatchItemPolicy>(
|
||||
[name UTF8String], path_type_pair.first, path_type_pair.second, allow_read_access, audit_only,
|
||||
std::get<ProcessList>(proc_list)));
|
||||
NSStringToUTF8StringView(name), path_type_pair.first, path_type_pair.second,
|
||||
allow_read_access, audit_only, invert_process_exceptions, std::get<ProcessList>(proc_list)));
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -520,24 +535,53 @@ bool ParseConfig(NSDictionary *config, std::vector<std::shared_ptr<WatchItemPoli
|
||||
|
||||
std::shared_ptr<WatchItems> WatchItems::Create(NSString *config_path,
|
||||
uint64_t reapply_config_frequency_secs) {
|
||||
return CreateInternal(config_path, nil, reapply_config_frequency_secs);
|
||||
}
|
||||
|
||||
std::shared_ptr<WatchItems> WatchItems::Create(NSDictionary *config,
|
||||
uint64_t reapply_config_frequency_secs) {
|
||||
return CreateInternal(nil, config, reapply_config_frequency_secs);
|
||||
}
|
||||
|
||||
std::shared_ptr<WatchItems> WatchItems::CreateInternal(NSString *config_path, NSDictionary *config,
|
||||
uint64_t reapply_config_frequency_secs) {
|
||||
if (reapply_config_frequency_secs < kMinReapplyConfigFrequencySecs) {
|
||||
LOGW(@"Invalid watch item update interval provided: %llu. Min allowed: %llu",
|
||||
reapply_config_frequency_secs, kMinReapplyConfigFrequencySecs);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (config_path && config) {
|
||||
LOGW(@"Invalid arguments creating WatchItems - both config and config_path cannot be set.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
dispatch_queue_t q = dispatch_queue_create("com.google.santa.daemon.watch_items.q",
|
||||
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
|
||||
dispatch_source_t timer_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
|
||||
dispatch_source_set_timer(timer_source, dispatch_time(DISPATCH_TIME_NOW, 0),
|
||||
NSEC_PER_SEC * reapply_config_frequency_secs, 0);
|
||||
|
||||
return std::make_shared<WatchItems>(config_path, q, timer_source);
|
||||
if (config_path) {
|
||||
return std::make_shared<WatchItems>(config_path, q, timer_source);
|
||||
} else {
|
||||
return std::make_shared<WatchItems>(config, q, timer_source);
|
||||
}
|
||||
}
|
||||
|
||||
WatchItems::WatchItems(NSString *config_path, dispatch_queue_t q, dispatch_source_t timer_source,
|
||||
void (^periodic_task_complete_f)(void))
|
||||
: config_path_(config_path),
|
||||
embedded_config_(nil),
|
||||
q_(q),
|
||||
timer_source_(timer_source),
|
||||
periodic_task_complete_f_(periodic_task_complete_f),
|
||||
watch_items_(std::make_unique<WatchItemsTree>()) {}
|
||||
|
||||
WatchItems::WatchItems(NSDictionary *config, dispatch_queue_t q, dispatch_source_t timer_source,
|
||||
void (^periodic_task_complete_f)(void))
|
||||
: config_path_(nil),
|
||||
embedded_config_(config),
|
||||
q_(q),
|
||||
timer_source_(timer_source),
|
||||
periodic_task_complete_f_(periodic_task_complete_f),
|
||||
@@ -616,7 +660,7 @@ void WatchItems::UpdateCurrentState(
|
||||
std::swap(currently_monitored_paths_, new_monitored_paths);
|
||||
current_config_ = new_config;
|
||||
if (new_config) {
|
||||
policy_version_ = [new_config[kWatchItemConfigKeyVersion] UTF8String];
|
||||
policy_version_ = NSStringToUTF8String(new_config[kWatchItemConfigKeyVersion]);
|
||||
} else {
|
||||
policy_version_ = "";
|
||||
}
|
||||
@@ -688,7 +732,7 @@ void WatchItems::BeginPeriodicTask() {
|
||||
return;
|
||||
}
|
||||
|
||||
shared_watcher->ReloadConfig(shared_watcher->ReadConfig());
|
||||
shared_watcher->ReloadConfig(embedded_config_ ?: shared_watcher->ReadConfig());
|
||||
|
||||
if (shared_watcher->periodic_task_complete_f_) {
|
||||
shared_watcher->periodic_task_complete_f_();
|
||||
@@ -718,11 +762,21 @@ void WatchItems::SetConfigPath(NSString *config_path) {
|
||||
{
|
||||
absl::MutexLock lock(&lock_);
|
||||
config_path_ = config_path;
|
||||
embedded_config_ = nil;
|
||||
config = ReadConfigLocked();
|
||||
}
|
||||
ReloadConfig(config);
|
||||
}
|
||||
|
||||
void WatchItems::SetConfig(NSDictionary *config) {
|
||||
{
|
||||
absl::MutexLock lock(&lock_);
|
||||
config_path_ = nil;
|
||||
embedded_config_ = config;
|
||||
}
|
||||
ReloadConfig(embedded_config_);
|
||||
}
|
||||
|
||||
std::optional<WatchItemsState> WatchItems::State() {
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
using santa::common::Unit;
|
||||
using santa::santad::data_layer::kWatchItemPolicyDefaultAllowReadAccess;
|
||||
using santa::santad::data_layer::kWatchItemPolicyDefaultAuditOnly;
|
||||
using santa::santad::data_layer::kWatchItemPolicyDefaultInvertProcessExceptions;
|
||||
using santa::santad::data_layer::kWatchItemPolicyDefaultPathType;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
using santa::santad::data_layer::WatchItemPolicy;
|
||||
@@ -61,8 +62,14 @@ extern std::variant<Unit, santatest::ProcessList> VerifyConfigWatchItemProcesses
|
||||
NSDictionary *watch_item, NSError **err);
|
||||
class WatchItemsPeer : public WatchItems {
|
||||
public:
|
||||
using WatchItems::ReloadConfig;
|
||||
using WatchItems::WatchItems;
|
||||
|
||||
using WatchItems::ReloadConfig;
|
||||
using WatchItems::SetConfig;
|
||||
using WatchItems::SetConfigPath;
|
||||
|
||||
using WatchItems::config_path_;
|
||||
using WatchItems::embedded_config_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::data_layer
|
||||
@@ -191,7 +198,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
// Changes in config dictionary will update policy info even if the
|
||||
// filesystem didn't change.
|
||||
{
|
||||
WatchItemsPeer watchItems(nil, NULL, NULL);
|
||||
WatchItemsPeer watchItems((NSString *)nil, NULL, NULL);
|
||||
[self pushd:@"a"];
|
||||
watchItems.ReloadConfig(configAllFilesOriginal);
|
||||
|
||||
@@ -212,7 +219,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
|
||||
// Changes to fileystem structure are reflected when a config is reloaded
|
||||
{
|
||||
WatchItemsPeer watchItems(nil, NULL, NULL);
|
||||
WatchItemsPeer watchItems((NSString *)nil, NULL, NULL);
|
||||
[self pushd:@"a"];
|
||||
watchItems.ReloadConfig(configAllFilesOriginal);
|
||||
[self popd];
|
||||
@@ -313,7 +320,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
}
|
||||
});
|
||||
|
||||
WatchItemsPeer watchItems(nil, NULL, NULL);
|
||||
WatchItemsPeer watchItems((NSString *)nil, NULL, NULL);
|
||||
WatchItems::VersionAndPolicies policies;
|
||||
|
||||
// Resultant vector is same size as input vector
|
||||
@@ -771,6 +778,17 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
},
|
||||
policies, &err));
|
||||
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsInvertProcessExceptions : @""}
|
||||
},
|
||||
policies, &err));
|
||||
XCTAssertTrue(ParseConfigSingleWatchItem(@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsInvertProcessExceptions : @(0)}
|
||||
},
|
||||
policies, &err));
|
||||
|
||||
// If processes are specified, they must be valid format
|
||||
// Note: Full tests in `testVerifyConfigWatchItemProcesses`
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(
|
||||
@@ -784,9 +802,11 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertTrue(
|
||||
ParseConfigSingleWatchItem(@"rule", @{kWatchItemConfigKeyPaths : @[ @"a" ]}, policies, &err));
|
||||
XCTAssertEqual(policies.size(), 1);
|
||||
XCTAssertEqual(*policies[0].get(), WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType,
|
||||
kWatchItemPolicyDefaultAllowReadAccess,
|
||||
kWatchItemPolicyDefaultAuditOnly, {}));
|
||||
XCTAssertEqual(
|
||||
*policies[0].get(),
|
||||
WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType,
|
||||
kWatchItemPolicyDefaultAllowReadAccess, kWatchItemPolicyDefaultAuditOnly,
|
||||
kWatchItemPolicyDefaultInvertProcessExceptions, {}));
|
||||
|
||||
// Test multiple paths, options, and processes
|
||||
policies.clear();
|
||||
@@ -800,7 +820,8 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
@[ @"a", @{kWatchItemConfigKeyPathsPath : @"b", kWatchItemConfigKeyPathsIsPrefix : @(YES)} ],
|
||||
kWatchItemConfigKeyOptions : @{
|
||||
kWatchItemConfigKeyOptionsAllowReadAccess : @(YES),
|
||||
kWatchItemConfigKeyOptionsAuditOnly : @(NO)
|
||||
kWatchItemConfigKeyOptionsAuditOnly : @(NO),
|
||||
kWatchItemConfigKeyOptionsInvertProcessExceptions : @(YES),
|
||||
},
|
||||
kWatchItemConfigKeyProcesses : @[
|
||||
@{kWatchItemConfigKeyProcessesBinaryPath : @"pa"},
|
||||
@@ -809,10 +830,10 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
},
|
||||
policies, &err));
|
||||
XCTAssertEqual(policies.size(), 2);
|
||||
XCTAssertEqual(*policies[0].get(),
|
||||
WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType, true, false, procs));
|
||||
XCTAssertEqual(*policies[1].get(),
|
||||
WatchItemPolicy("rule", "b", WatchItemPathType::kPrefix, true, false, procs));
|
||||
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));
|
||||
}
|
||||
|
||||
- (void)testState {
|
||||
@@ -842,4 +863,23 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertGreaterThanOrEqual(state.last_config_load_epoch, startTime);
|
||||
}
|
||||
|
||||
- (void)testSetConfigAndSetConfigPath {
|
||||
// Test internal state when switching back and forth between path-based and
|
||||
// dictionary-based config options.
|
||||
WatchItemsPeer watchItems(@{}, NULL, NULL);
|
||||
|
||||
XCTAssertNil(watchItems.config_path_);
|
||||
XCTAssertNotNil(watchItems.embedded_config_);
|
||||
|
||||
watchItems.SetConfigPath(@"/path/to/a/nonexistent/file/so/nothing/is/opened");
|
||||
|
||||
XCTAssertNotNil(watchItems.config_path_);
|
||||
XCTAssertNil(watchItems.embedded_config_);
|
||||
|
||||
watchItems.SetConfig(@{});
|
||||
|
||||
XCTAssertNil(watchItems.config_path_);
|
||||
XCTAssertNotNil(watchItems.embedded_config_);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#include "Source/common/SantaCache.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
@@ -33,16 +34,29 @@ enum class FlushCacheMode {
|
||||
kAllCaches,
|
||||
};
|
||||
|
||||
enum class FlushCacheReason {
|
||||
kClientModeChanged,
|
||||
kPathRegexChanged,
|
||||
kRulesChanged,
|
||||
kStaticRulesChanged,
|
||||
kExplicitCommand,
|
||||
kFilesystemUnmounted,
|
||||
};
|
||||
|
||||
class AuthResultCache {
|
||||
public:
|
||||
// Santa currently only flushes caches when new DENY rules are added, not
|
||||
// ALLOW rules. This means this value should be low enough so that if a
|
||||
// ALLOW rules. This means cache_deny_time_ms should be low enough so that if a
|
||||
// previously denied binary is allowed, it can be re-executed by the user in a
|
||||
// timely manner. But the value should be high enough to allow the cache to be
|
||||
// effective in the event the binary is executed in rapid succession.
|
||||
static std::unique_ptr<AuthResultCache> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
SNTMetricSet *metric_set, uint64_t cache_deny_time_ms = 1500);
|
||||
|
||||
AuthResultCache(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms = 1500);
|
||||
SNTMetricCounter *flush_count, uint64_t cache_deny_time_ms = 1500);
|
||||
virtual ~AuthResultCache();
|
||||
|
||||
AuthResultCache(AuthResultCache &&other) = delete;
|
||||
@@ -55,7 +69,7 @@ class AuthResultCache {
|
||||
virtual SNTAction CheckCache(const es_file_t *es_file);
|
||||
virtual SNTAction CheckCache(SantaVnode vnode_id);
|
||||
|
||||
virtual void FlushCache(FlushCacheMode mode);
|
||||
virtual void FlushCache(FlushCacheMode mode, FlushCacheReason reason);
|
||||
|
||||
virtual NSArray<NSNumber *> *CacheCounts();
|
||||
|
||||
@@ -66,6 +80,7 @@ class AuthResultCache {
|
||||
SantaCache<SantaVnode, uint64_t> *nonroot_cache_;
|
||||
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
|
||||
SNTMetricCounter *flush_count_;
|
||||
uint64_t root_devno_;
|
||||
uint64_t cache_deny_time_ns_;
|
||||
dispatch_queue_t q_;
|
||||
|
||||
@@ -25,6 +25,13 @@
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
|
||||
static NSString *const kFlushCacheReasonClientModeChanged = @"ClientModeChanged";
|
||||
static NSString *const kFlushCacheReasonPathRegexChanged = @"PathRegexChanged";
|
||||
static NSString *const kFlushCacheReasonRulesChanged = @"RulesChanged";
|
||||
static NSString *const kFlushCacheReasonStaticRulesChanged = @"StaticRulesChanged";
|
||||
static NSString *const kFlushCacheReasonExplicitCommand = @"ExplicitCommand";
|
||||
static NSString *const kFlushCacheReasonFilesystemUnmounted = @"FilesystemUnmounted";
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
static inline uint64_t GetCurrentUptime() {
|
||||
@@ -44,9 +51,36 @@ static inline uint64_t TimestampFromCachedValue(uint64_t cachedValue) {
|
||||
return (cachedValue & ~(0xFF00000000000000));
|
||||
}
|
||||
|
||||
NSString *const FlushCacheReasonToString(FlushCacheReason reason) {
|
||||
switch (reason) {
|
||||
case FlushCacheReason::kClientModeChanged: return kFlushCacheReasonClientModeChanged;
|
||||
case FlushCacheReason::kPathRegexChanged: return kFlushCacheReasonPathRegexChanged;
|
||||
case FlushCacheReason::kRulesChanged: return kFlushCacheReasonRulesChanged;
|
||||
case FlushCacheReason::kStaticRulesChanged: return kFlushCacheReasonStaticRulesChanged;
|
||||
case FlushCacheReason::kExplicitCommand: return kFlushCacheReasonExplicitCommand;
|
||||
case FlushCacheReason::kFilesystemUnmounted: return kFlushCacheReasonFilesystemUnmounted;
|
||||
default:
|
||||
[NSException raise:@"Invalid reason" format:@"Unknown reason value: %d", reason];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<AuthResultCache> AuthResultCache::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTMetricSet *metric_set,
|
||||
uint64_t cache_deny_time_ms) {
|
||||
SNTMetricCounter *flush_count =
|
||||
[metric_set counterWithName:@"/santa/flush_count"
|
||||
fieldNames:@[ @"Reason" ]
|
||||
helpText:@"Count of times the auth result cache is flushed by reason"];
|
||||
|
||||
return std::make_unique<AuthResultCache>(esapi, flush_count, cache_deny_time_ms);
|
||||
}
|
||||
|
||||
AuthResultCache::AuthResultCache(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms)
|
||||
: esapi_(esapi), cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
|
||||
SNTMetricCounter *flush_count, uint64_t cache_deny_time_ms)
|
||||
: esapi_(esapi),
|
||||
flush_count_(flush_count),
|
||||
cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
|
||||
root_cache_ = new SantaCache<SantaVnode, uint64_t>();
|
||||
nonroot_cache_ = new SantaCache<SantaVnode, uint64_t>();
|
||||
|
||||
@@ -118,7 +152,7 @@ SantaCache<SantaVnode, uint64_t> *AuthResultCache::CacheForVnodeID(SantaVnode vn
|
||||
return (vnode_id.fsid == root_devno_ || root_devno_ == 0) ? root_cache_ : nonroot_cache_;
|
||||
}
|
||||
|
||||
void AuthResultCache::FlushCache(FlushCacheMode mode) {
|
||||
void AuthResultCache::FlushCache(FlushCacheMode mode, FlushCacheReason reason) {
|
||||
nonroot_cache_->clear();
|
||||
if (mode == FlushCacheMode::kAllCaches) {
|
||||
root_cache_->clear();
|
||||
@@ -134,6 +168,8 @@ void AuthResultCache::FlushCache(FlushCacheMode mode) {
|
||||
shared_esapi->ClearCache(Client());
|
||||
});
|
||||
}
|
||||
|
||||
[flush_count_ incrementForFieldValues:@[ FlushCacheReasonToString(reason) ]];
|
||||
}
|
||||
|
||||
NSArray<NSNumber *> *AuthResultCache::CacheCounts() {
|
||||
|
||||
@@ -29,6 +29,13 @@
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::FlushCacheReason;
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
extern NSString *const FlushCacheReasonToString(FlushCacheReason reason);
|
||||
}
|
||||
|
||||
using santa::santad::event_providers::FlushCacheReasonToString;
|
||||
|
||||
// Grab the st_dev number of the root volume to match the root cache
|
||||
static uint64_t RootDevno() {
|
||||
@@ -66,14 +73,14 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
|
||||
- (void)testEmptyCacheExpectedNumberOfCacheCounts {
|
||||
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(esapi);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(esapi, nil);
|
||||
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
- (void)testBasicOperation {
|
||||
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(esapi);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(esapi, nil);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 222);
|
||||
@@ -110,7 +117,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
|
||||
- (void)testFlushCache {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(mockESApi, nil);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 111);
|
||||
@@ -121,7 +128,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
|
||||
// Flush non-root only
|
||||
cache->FlushCache(FlushCacheMode::kNonRootOnly);
|
||||
cache->FlushCache(FlushCacheMode::kNonRootOnly, FlushCacheReason::kClientModeChanged);
|
||||
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
|
||||
@@ -138,7 +145,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
dispatch_semaphore_signal(sema);
|
||||
return true;
|
||||
}));
|
||||
cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
cache->FlushCache(FlushCacheMode::kAllCaches, FlushCacheReason::kClientModeChanged);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
|
||||
@@ -151,7 +158,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
|
||||
- (void)testCacheStateMachine {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(mockESApi, nil);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
@@ -193,7 +200,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
// Create a cache with a lowered cache expiry value
|
||||
uint64_t expiryMS = 250;
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi, expiryMS);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(mockESApi, nil, expiryMS);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
@@ -215,4 +222,21 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
- (void)testFlushCacheReasonToString {
|
||||
std::map<FlushCacheReason, NSString *> reasonToString = {
|
||||
{FlushCacheReason::kClientModeChanged, @"ClientModeChanged"},
|
||||
{FlushCacheReason::kPathRegexChanged, @"PathRegexChanged"},
|
||||
{FlushCacheReason::kRulesChanged, @"RulesChanged"},
|
||||
{FlushCacheReason::kStaticRulesChanged, @"StaticRulesChanged"},
|
||||
{FlushCacheReason::kExplicitCommand, @"ExplicitCommand"},
|
||||
{FlushCacheReason::kFilesystemUnmounted, @"FilesystemUnmounted"},
|
||||
};
|
||||
|
||||
for (const auto &kv : reasonToString) {
|
||||
XCTAssertEqualObjects(FlushCacheReasonToString(kv.first), kv.second);
|
||||
}
|
||||
|
||||
XCTAssertThrows(FlushCacheReasonToString((FlushCacheReason)12345));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
|
||||
@@ -46,7 +46,11 @@ class EnrichedFile {
|
||||
group_(std::move(other.group_)),
|
||||
hash_(std::move(other.hash_)) {}
|
||||
|
||||
// Note: Move assignment could be safely implemented but not currently needed
|
||||
EnrichedFile &operator=(EnrichedFile &&other) = delete;
|
||||
|
||||
EnrichedFile(const EnrichedFile &other) = delete;
|
||||
EnrichedFile &operator=(const EnrichedFile &other) = delete;
|
||||
|
||||
const std::optional<std::shared_ptr<std::string>> &user() const {
|
||||
return user_;
|
||||
@@ -87,7 +91,11 @@ class EnrichedProcess {
|
||||
real_group_(std::move(other.real_group_)),
|
||||
executable_(std::move(other.executable_)) {}
|
||||
|
||||
// Note: Move assignment could be safely implemented but not currently needed
|
||||
EnrichedProcess &operator=(EnrichedProcess &&other) = delete;
|
||||
|
||||
EnrichedProcess(const EnrichedProcess &other) = delete;
|
||||
EnrichedProcess &operator=(const EnrichedProcess &other) = delete;
|
||||
|
||||
const std::optional<std::shared_ptr<std::string>> &effective_user() const {
|
||||
return effective_user_;
|
||||
@@ -123,7 +131,12 @@ class EnrichedEventType {
|
||||
instigator_(std::move(other.instigator_)),
|
||||
enrichment_time_(std::move(other.enrichment_time_)) {}
|
||||
|
||||
// Note: Move assignment could be safely implemented but not currently needed
|
||||
// so no sense in implementing across all child classes
|
||||
EnrichedEventType &operator=(EnrichedEventType &&other) = delete;
|
||||
|
||||
EnrichedEventType(const EnrichedEventType &other) = delete;
|
||||
EnrichedEventType &operator=(const EnrichedEventType &other) = delete;
|
||||
|
||||
virtual ~EnrichedEventType() = default;
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class Enricher {
|
||||
public:
|
||||
Enricher();
|
||||
virtual ~Enricher() = default;
|
||||
virtual std::shared_ptr<EnrichedMessage> Enrich(Message &&msg);
|
||||
virtual std::unique_ptr<EnrichedMessage> Enrich(Message &&msg);
|
||||
virtual EnrichedProcess Enrich(
|
||||
const es_process_t &es_proc,
|
||||
EnrichOptions options = EnrichOptions::kDefault);
|
||||
|
||||
@@ -30,19 +30,19 @@ namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Enricher::Enricher() : username_cache_(256), groupname_cache_(256) {}
|
||||
|
||||
std::shared_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
|
||||
std::unique_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
|
||||
// TODO(mlw): Consider potential design patterns that could help reduce memory usage under load
|
||||
// (such as maybe the flyweight pattern)
|
||||
switch (es_msg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedClose(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedClose(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.close.target)));
|
||||
case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedExchange(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedExchange(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.exchangedata.file1),
|
||||
Enrich(*es_msg->event.exchangedata.file2)));
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedExec(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedExec(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.exec.target),
|
||||
(es_msg->version >= 2 && es_msg->event.exec.script)
|
||||
? std::make_optional(Enrich(*es_msg->event.exec.script))
|
||||
@@ -51,28 +51,28 @@ std::shared_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
|
||||
? std::make_optional(Enrich(*es_msg->event.exec.cwd))
|
||||
: std::nullopt));
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedFork(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedFork(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.fork.child)));
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT:
|
||||
return std::make_shared<EnrichedMessage>(
|
||||
return std::make_unique<EnrichedMessage>(
|
||||
EnrichedExit(std::move(es_msg), Enrich(*es_msg->process)));
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK:
|
||||
return std::make_shared<EnrichedMessage>(
|
||||
return std::make_unique<EnrichedMessage>(
|
||||
EnrichedLink(std::move(es_msg), Enrich(*es_msg->process),
|
||||
Enrich(*es_msg->event.link.source), Enrich(*es_msg->event.link.target_dir)));
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: {
|
||||
if (es_msg->event.rename.destination_type == ES_DESTINATION_TYPE_NEW_PATH) {
|
||||
return std::make_shared<EnrichedMessage>(EnrichedRename(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedRename(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.rename.source),
|
||||
std::nullopt, Enrich(*es_msg->event.rename.destination.new_path.dir)));
|
||||
} else {
|
||||
return std::make_shared<EnrichedMessage>(EnrichedRename(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedRename(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.rename.source),
|
||||
Enrich(*es_msg->event.rename.destination.existing_file), std::nullopt));
|
||||
}
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedUnlink(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedUnlink(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.unlink.target)));
|
||||
default:
|
||||
// This is a programming error
|
||||
|
||||
77
Source/santad/EventProviders/RateLimiter.h
Normal file
77
Source/santad/EventProviders/RateLimiter.h
Normal file
@@ -0,0 +1,77 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_RATELIMITER_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_RATELIMITER_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
// Forward declarations
|
||||
namespace santa::santad::event_providers {
|
||||
class RateLimiterPeer;
|
||||
}
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
// Very basic rate limiting infrastructure.
|
||||
// Currently only handles X events per duration.
|
||||
//
|
||||
// TODO(mlw): Support changing QPS via config
|
||||
// TODO(mlw): Support per-rule QPS
|
||||
// TODO(mlw): Consider adding sliding window support
|
||||
class RateLimiter {
|
||||
public:
|
||||
// Factory
|
||||
static std::shared_ptr<RateLimiter> Create(
|
||||
std::shared_ptr<santa::santad::Metrics> metrics,
|
||||
santa::santad::Processor processor, uint16_t max_qps,
|
||||
NSTimeInterval reset_duration = kDefaultResetDuration);
|
||||
|
||||
RateLimiter(std::shared_ptr<santa::santad::Metrics> metrics,
|
||||
santa::santad::Processor processor, uint16_t max_qps,
|
||||
NSTimeInterval reset_duration);
|
||||
|
||||
enum class Decision {
|
||||
kRateLimited = 0,
|
||||
kAllowed,
|
||||
};
|
||||
|
||||
Decision Decide(uint64_t cur_mach_time);
|
||||
|
||||
friend class santa::santad::event_providers::RateLimiterPeer;
|
||||
|
||||
private:
|
||||
bool ShouldRateLimitLocked();
|
||||
size_t EventsRateLimitedLocked();
|
||||
void TryResetLocked(uint64_t cur_mach_time);
|
||||
|
||||
static constexpr NSTimeInterval kDefaultResetDuration = 15.0;
|
||||
|
||||
std::shared_ptr<santa::santad::Metrics> metrics_;
|
||||
santa::santad::Processor processor_;
|
||||
size_t log_count_total_ = 0;
|
||||
size_t max_log_count_total_;
|
||||
uint64_t reset_mach_time_;
|
||||
uint64_t reset_duration_ns_;
|
||||
dispatch_queue_t q_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers
|
||||
|
||||
#endif
|
||||
85
Source/santad/EventProviders/RateLimiter.mm
Normal file
85
Source/santad/EventProviders/RateLimiter.mm
Normal file
@@ -0,0 +1,85 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/EventProviders/RateLimiter.h"
|
||||
|
||||
#include "Source/common/BranchPrediction.h"
|
||||
#include "Source/common/SystemResources.h"
|
||||
|
||||
using santa::santad::Metrics;
|
||||
using santa::santad::Processor;
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
std::shared_ptr<RateLimiter> RateLimiter::Create(std::shared_ptr<Metrics> metrics,
|
||||
Processor processor, uint16_t max_qps,
|
||||
NSTimeInterval reset_duration) {
|
||||
return std::make_shared<RateLimiter>(std::move(metrics), processor, max_qps, reset_duration);
|
||||
}
|
||||
|
||||
RateLimiter::RateLimiter(std::shared_ptr<Metrics> metrics, Processor processor, uint16_t max_qps,
|
||||
NSTimeInterval reset_duration)
|
||||
: metrics_(std::move(metrics)),
|
||||
processor_(processor),
|
||||
max_log_count_total_(reset_duration * max_qps),
|
||||
reset_mach_time_(0),
|
||||
reset_duration_ns_(reset_duration * NSEC_PER_SEC) {
|
||||
q_ = dispatch_queue_create(
|
||||
"com.google.santa.daemon.rate_limiter",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
|
||||
QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
}
|
||||
|
||||
bool RateLimiter::ShouldRateLimitLocked() {
|
||||
return log_count_total_ > max_log_count_total_;
|
||||
}
|
||||
|
||||
size_t RateLimiter::EventsRateLimitedLocked() {
|
||||
if (unlikely(ShouldRateLimitLocked())) {
|
||||
return log_count_total_ - max_log_count_total_;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void RateLimiter::TryResetLocked(uint64_t cur_mach_time) {
|
||||
if (cur_mach_time > reset_mach_time_) {
|
||||
if (metrics_) {
|
||||
metrics_->SetRateLimitingMetrics(processor_, EventsRateLimitedLocked());
|
||||
}
|
||||
|
||||
log_count_total_ = 0;
|
||||
reset_mach_time_ = AddNanosecondsToMachTime(reset_duration_ns_, cur_mach_time);
|
||||
}
|
||||
}
|
||||
|
||||
RateLimiter::Decision RateLimiter::Decide(uint64_t cur_mach_time) {
|
||||
__block RateLimiter::Decision decision;
|
||||
|
||||
dispatch_sync(q_, ^{
|
||||
TryResetLocked(cur_mach_time);
|
||||
|
||||
++log_count_total_;
|
||||
|
||||
if (unlikely(ShouldRateLimitLocked())) {
|
||||
decision = Decision::kRateLimited;
|
||||
} else {
|
||||
decision = RateLimiter::Decision::kAllowed;
|
||||
}
|
||||
});
|
||||
|
||||
return decision;
|
||||
}
|
||||
|
||||
} // namespace santa::santad::event_providers
|
||||
149
Source/santad/EventProviders/RateLimiterTest.mm
Normal file
149
Source/santad/EventProviders/RateLimiterTest.mm
Normal file
@@ -0,0 +1,149 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/EventProviders/RateLimiter.h"
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#include "Source/common/SystemResources.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
using santa::santad::event_providers::RateLimiter;
|
||||
|
||||
static const santa::santad::Processor kDefaultProcessor =
|
||||
santa::santad::Processor::kFileAccessAuthorizer;
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
class RateLimiterPeer : public RateLimiter {
|
||||
public:
|
||||
using RateLimiter::RateLimiter;
|
||||
|
||||
using RateLimiter::EventsRateLimitedLocked;
|
||||
using RateLimiter::ShouldRateLimitLocked;
|
||||
using RateLimiter::TryResetLocked;
|
||||
|
||||
using RateLimiter::log_count_total_;
|
||||
using RateLimiter::reset_mach_time_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers
|
||||
|
||||
using santa::santad::event_providers::RateLimiterPeer;
|
||||
|
||||
@interface RateLimiterTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation RateLimiterTest
|
||||
|
||||
- (void)testTryResetLocked {
|
||||
// Create an object supporting 1 QPS, and a reset duration of 2s
|
||||
uint16_t maxQps = 1;
|
||||
NSTimeInterval resetDuration = 2;
|
||||
RateLimiterPeer rlp(nullptr, kDefaultProcessor, maxQps, resetDuration);
|
||||
|
||||
// Check the current reset_mach_time_ is 0 so that it gets
|
||||
// set when the first decision is made
|
||||
XCTAssertEqual(rlp.reset_mach_time_, 0);
|
||||
|
||||
// Define our current mach time and create the expected new reset duration floor
|
||||
uint64_t curMachTime = 1;
|
||||
uint64_t expectedMachTime = AddNanosecondsToMachTime(resetDuration, curMachTime);
|
||||
|
||||
// Set a higher log count to ensure it is reset
|
||||
rlp.log_count_total_ = 123;
|
||||
|
||||
rlp.TryResetLocked(curMachTime);
|
||||
|
||||
// Ensure values are reset appropriately
|
||||
XCTAssertEqual(rlp.log_count_total_, 0);
|
||||
XCTAssertGreaterThanOrEqual(rlp.reset_mach_time_, expectedMachTime);
|
||||
|
||||
// Setup values so that calling TryResetLocked shouldn't reset anything
|
||||
size_t expectedLogCount = 123;
|
||||
expectedMachTime = 456;
|
||||
rlp.log_count_total_ = expectedLogCount;
|
||||
rlp.reset_mach_time_ = expectedMachTime;
|
||||
curMachTime = rlp.reset_mach_time_;
|
||||
|
||||
rlp.TryResetLocked(curMachTime);
|
||||
|
||||
// Ensure the values were not changed
|
||||
XCTAssertEqual(rlp.log_count_total_, expectedLogCount);
|
||||
XCTAssertGreaterThanOrEqual(rlp.reset_mach_time_, expectedMachTime);
|
||||
}
|
||||
|
||||
- (void)testDecide {
|
||||
// Create an object supporting 2 QPS, and a reset duration of 4s
|
||||
uint16_t maxQps = 2;
|
||||
NSTimeInterval resetDuration = 4;
|
||||
uint64_t allowedLogsPerDuration = maxQps * resetDuration;
|
||||
RateLimiterPeer rlp(nullptr, kDefaultProcessor, maxQps, resetDuration);
|
||||
|
||||
// Check the current log count is initially 0
|
||||
XCTAssertEqual(rlp.log_count_total_, 0);
|
||||
|
||||
// Make the first decision
|
||||
RateLimiter::Decision gotDecision;
|
||||
|
||||
for (uint64_t i = 0; i < (allowedLogsPerDuration); i++) {
|
||||
gotDecision = rlp.Decide(0);
|
||||
XCTAssertEqual(gotDecision, RateLimiter::Decision::kAllowed);
|
||||
}
|
||||
|
||||
// Ensure the log count is the expected amount
|
||||
XCTAssertEqual(rlp.log_count_total_, allowedLogsPerDuration);
|
||||
|
||||
// Make another decision and ensure the log count still increases and
|
||||
// the decision is rate limited
|
||||
gotDecision = rlp.Decide(0);
|
||||
XCTAssertEqual(gotDecision, RateLimiter::Decision::kRateLimited);
|
||||
XCTAssertEqual(rlp.log_count_total_, allowedLogsPerDuration + 1);
|
||||
|
||||
// Make another decision, though now with the cur mach time greater than
|
||||
// the reset mach time. Then ensure values were appropriately reset.
|
||||
uint64_t oldResetMachTime = rlp.reset_mach_time_;
|
||||
gotDecision = rlp.Decide(rlp.reset_mach_time_ + 1);
|
||||
XCTAssertEqual(gotDecision, RateLimiter::Decision::kAllowed);
|
||||
XCTAssertEqual(rlp.log_count_total_, 1);
|
||||
XCTAssertGreaterThan(rlp.reset_mach_time_, oldResetMachTime);
|
||||
}
|
||||
|
||||
- (void)testShouldRateLimitAndCounts {
|
||||
// Create an object supporting 2 QPS, and a reset duration of 4s
|
||||
uint16_t maxQps = 2;
|
||||
NSTimeInterval resetDuration = 4;
|
||||
uint64_t allowedLogsPerDuration = maxQps * resetDuration;
|
||||
uint64_t logsOverQPS = 5;
|
||||
RateLimiterPeer rlp(nullptr, kDefaultProcessor, maxQps, resetDuration);
|
||||
|
||||
// Initially no rate limiting should apply
|
||||
XCTAssertFalse(rlp.ShouldRateLimitLocked());
|
||||
XCTAssertEqual(rlp.EventsRateLimitedLocked(), 0);
|
||||
|
||||
// Simulate a smmaller volume of logs received than QPS
|
||||
rlp.log_count_total_ = allowedLogsPerDuration - 1;
|
||||
|
||||
XCTAssertFalse(rlp.ShouldRateLimitLocked());
|
||||
XCTAssertEqual(rlp.EventsRateLimitedLocked(), 0);
|
||||
|
||||
// Simulate a larger volume of logs received than QPS
|
||||
rlp.log_count_total_ = allowedLogsPerDuration + logsOverQPS;
|
||||
|
||||
XCTAssertTrue(rlp.ShouldRateLimitLocked());
|
||||
XCTAssertEqual(rlp.EventsRateLimitedLocked(), logsOverQPS);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -85,6 +85,15 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
}
|
||||
|
||||
- (void)testHandleMessage {
|
||||
#ifdef THREAD_SANITIZER
|
||||
// TSAN and this test do not get along in multiple ways.
|
||||
// We get data race false positives in OCMock, and timeouts
|
||||
// waiting for messages processing (presumably due to tsan's scheduling).
|
||||
// Just skip it.
|
||||
XCTSkip(@"TSAN enabled");
|
||||
return;
|
||||
#endif
|
||||
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth);
|
||||
@@ -137,27 +146,31 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
Message msg(mockESApi, &esMsg);
|
||||
// Scope so msg is destructed (and calls ReleaseMessage) before stopMocking is called.
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(NO);
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(NO);
|
||||
|
||||
OCMExpect([mockAuthClient postAction:SNTActionRespondDeny
|
||||
OCMExpect([mockAuthClient postAction:SNTActionRespondDeny
|
||||
forMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs();
|
||||
OCMStub([mockAuthClient postAction:SNTActionRespondDeny
|
||||
forMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs();
|
||||
OCMStub([mockAuthClient postAction:SNTActionRespondDeny forMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(nil);
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(nil);
|
||||
|
||||
[mockAuthClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
[mockAuthClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
}
|
||||
|
||||
[mockAuthClient stopMocking];
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
@@ -180,25 +193,27 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
Message msg(mockESApi, &esMsg);
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(YES);
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(YES);
|
||||
|
||||
OCMExpect([mockAuthClient processMessage:Message(mockESApi, &esMsg)]).ignoringNonObjectArgs();
|
||||
OCMStub([mockAuthClient processMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(nil);
|
||||
OCMExpect([mockAuthClient processMessage:Message(mockESApi, &esMsg)]).ignoringNonObjectArgs();
|
||||
OCMStub([mockAuthClient processMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(nil);
|
||||
|
||||
[mockAuthClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kProcessed);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
[mockAuthClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kProcessed);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
}
|
||||
|
||||
[mockAuthClient stopMocking];
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
@@ -219,7 +234,7 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
EXPECT_CALL(*mockAuthCache, CheckCache)
|
||||
.WillOnce(testing::Return(SNTActionRequestBinary))
|
||||
.WillOnce(testing::Return(SNTActionRequestBinary))
|
||||
@@ -286,7 +301,7 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, SNTActionRespondAllowCompiler))
|
||||
.WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, SNTActionRespondAllow))
|
||||
|
||||
@@ -193,13 +193,12 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
return _esApi->UnsubscribeAll(_esClient);
|
||||
}
|
||||
|
||||
- (bool)unmuteEverything {
|
||||
bool result = _esApi->UnmuteAllPaths(_esClient);
|
||||
result = _esApi->UnmuteAllTargetPaths(_esClient) && result;
|
||||
return result;
|
||||
- (bool)unmuteAllTargetPaths {
|
||||
return _esApi->UnmuteAllTargetPaths(_esClient);
|
||||
}
|
||||
|
||||
- (bool)enableTargetPathWatching {
|
||||
[self unmuteAllTargetPaths];
|
||||
return _esApi->InvertTargetPathMuting(_esClient);
|
||||
}
|
||||
|
||||
@@ -236,9 +235,9 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
}
|
||||
}
|
||||
|
||||
- (void)processEnrichedMessage:(std::shared_ptr<EnrichedMessage>)msg
|
||||
handler:(void (^)(std::shared_ptr<EnrichedMessage>))messageHandler {
|
||||
__block std::shared_ptr<EnrichedMessage> msgTmp = std::move(msg);
|
||||
- (void)processEnrichedMessage:(std::unique_ptr<EnrichedMessage>)msg
|
||||
handler:(void (^)(std::unique_ptr<EnrichedMessage>))messageHandler {
|
||||
__block std::unique_ptr<EnrichedMessage> msgTmp = std::move(msg);
|
||||
dispatch_async(_notifyQueue, ^{
|
||||
messageHandler(std::move(msgTmp));
|
||||
});
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
- (bool)subscribeAndClearCache:(const std::set<es_event_type_t> &)events;
|
||||
|
||||
- (bool)unsubscribeAll;
|
||||
- (bool)unmuteEverything;
|
||||
- (bool)unmuteAllTargetPaths;
|
||||
- (bool)enableTargetPathWatching;
|
||||
- (bool)muteTargetPaths:
|
||||
(const std::vector<std::pair<std::string, santa::santad::data_layer::WatchItemPathType>> &)paths;
|
||||
@@ -72,9 +72,9 @@
|
||||
|
||||
- (void)
|
||||
processEnrichedMessage:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage>)msg
|
||||
(std::unique_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage>)msg
|
||||
handler:
|
||||
(void (^)(std::shared_ptr<
|
||||
(void (^)(std::unique_ptr<
|
||||
santa::santad::event_providers::endpoint_security::EnrichedMessage>))
|
||||
messageHandler;
|
||||
|
||||
|
||||
@@ -274,23 +274,20 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testUnmuteEverything {
|
||||
- (void)testUnmuteAllTargetPaths {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
// Test variations of underlying unmute impls returning both true and false
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllPaths)
|
||||
.WillOnce(testing::Return(true))
|
||||
.WillOnce(testing::Return(false));
|
||||
// Test the underlying unmute impl returning both true and false
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths)
|
||||
.WillOnce(testing::Return(true))
|
||||
.WillOnce(testing::Return(true));
|
||||
.WillOnce(testing::Return(false));
|
||||
|
||||
XCTAssertTrue([client unmuteEverything]);
|
||||
XCTAssertFalse([client unmuteEverything]);
|
||||
XCTAssertTrue([client unmuteAllTargetPaths]);
|
||||
XCTAssertFalse([client unmuteAllTargetPaths]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
@@ -302,6 +299,9 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
// UnmuteAllTargetPaths is always attempted.
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).Times(2).WillRepeatedly(testing::Return(true));
|
||||
|
||||
// Test the underlying invert nute impl returning both true and false
|
||||
EXPECT_CALL(*mockESApi, InvertTargetPathMuting)
|
||||
.WillOnce(testing::Return(true))
|
||||
@@ -406,14 +406,21 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
{
|
||||
auto enrichedMsg = std::make_shared<EnrichedMessage>(
|
||||
auto enrichedMsg = std::make_unique<EnrichedMessage>(
|
||||
EnrichedClose(Message(mockESApi, &esMsg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
|
||||
[client processEnrichedMessage:enrichedMsg
|
||||
handler:^(std::shared_ptr<EnrichedMessage> msg) {
|
||||
[client processEnrichedMessage:std::move(enrichedMsg)
|
||||
handler:^(std::unique_ptr<EnrichedMessage> msg) {
|
||||
// reset the shared_ptr to drop the held message.
|
||||
// This is a workaround for a TSAN only false positive
|
||||
// which happens if we switch back to the sem wait
|
||||
// after signaling, but _before_ the implicit release
|
||||
// of msg. In that case, the mock verify and the
|
||||
// call of the mock's Release method can data race.
|
||||
msg.reset();
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::FlushCacheReason;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
@@ -172,6 +173,15 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
- (void)handleMessage:(Message &&)esMsg
|
||||
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
|
||||
// Process the unmount event first so that caches are flushed before any
|
||||
// other potential early returns.
|
||||
if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_UNMOUNT) {
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kNonRootOnly,
|
||||
FlushCacheReason::kFilesystemUnmounted);
|
||||
recordEventMetrics(EventDisposition::kProcessed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.blockUSBMount) {
|
||||
// TODO: We should also unsubscribe from events when this isn't set, but
|
||||
// this is generally a low-volume event type.
|
||||
@@ -180,12 +190,6 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return;
|
||||
}
|
||||
|
||||
if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_UNMOUNT) {
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kNonRootOnly);
|
||||
recordEventMetrics(EventDisposition::kProcessed);
|
||||
return;
|
||||
}
|
||||
|
||||
[self processMessage:std::move(esMsg)
|
||||
handler:^(const Message &msg) {
|
||||
es_auth_result_t result = [self handleAuthMount:msg];
|
||||
@@ -248,10 +252,17 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@"isEjectable: %d",
|
||||
protocol, kind, isInternal, isRemovable, isEjectable);
|
||||
|
||||
// If the device is internal or virtual we are okay with the operation. We
|
||||
// also are okay with operations for devices that are non-removal as long as
|
||||
// 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 (isInternal || isVirtual || (!isRemovable && !isEjectable && !isUSB && !isSecureDigital)) {
|
||||
if (!isRemovable && !isEjectable && !isUSB && !isSecureDigital) {
|
||||
return ES_AUTH_RESULT_ALLOW;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,13 +39,14 @@
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::FlushCacheReason;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
class MockAuthResultCache : public AuthResultCache {
|
||||
public:
|
||||
using AuthResultCache::AuthResultCache;
|
||||
|
||||
MOCK_METHOD(void, FlushCache, (FlushCacheMode mode));
|
||||
MOCK_METHOD(void, FlushCache, (FlushCacheMode mode, FlushCacheReason reason));
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManager (Testing)
|
||||
@@ -316,7 +317,7 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
EXPECT_CALL(*mockAuthCache, FlushCache);
|
||||
|
||||
SNTEndpointSecurityDeviceManager *deviceManager =
|
||||
|
||||
@@ -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"
|
||||
@@ -25,6 +26,8 @@
|
||||
#include "Source/santad/Metrics.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
typedef void (^SNTFileAccessBlockCallback)(SNTFileAccessEvent *event);
|
||||
|
||||
@interface SNTEndpointSecurityFileAccessAuthorizer
|
||||
: SNTEndpointSecurityClient <SNTEndpointSecurityDynamicEventHandler>
|
||||
|
||||
@@ -38,4 +41,6 @@
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
|
||||
decisionCache:(SNTDecisionCache *)decisionCache;
|
||||
|
||||
@property SNTFileAccessBlockCallback fileAccessBlockCallback;
|
||||
|
||||
@end
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <sys/fcntl.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -32,19 +33,25 @@
|
||||
#include "Source/common/Platform.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"
|
||||
|
||||
using santa::common::StringToNSString;
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
using santa::santad::data_layer::WatchItemPolicy;
|
||||
using santa::santad::data_layer::WatchItems;
|
||||
using santa::santad::event_providers::RateLimiter;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichOptions;
|
||||
@@ -54,6 +61,7 @@ using santa::santad::logs::endpoint_security::Logger;
|
||||
NSString *kBadCertHash = @"BAD_CERT_HASH";
|
||||
|
||||
static constexpr uint32_t kOpenFlagsIndicatingWrite = FWRITE | O_APPEND | O_TRUNC;
|
||||
static constexpr uint16_t kDefaultRateLimitQPS = 50;
|
||||
|
||||
// Small structure to hold a complete event path target being operated upon and
|
||||
// a bool indicating whether the path is a readable target (e.g. a file being
|
||||
@@ -191,13 +199,13 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
@interface SNTEndpointSecurityFileAccessAuthorizer ()
|
||||
@property SNTDecisionCache *decisionCache;
|
||||
@property bool isSubscribed;
|
||||
@property SNTMetricBooleanGauge *famEnabled;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityFileAccessAuthorizer {
|
||||
std::shared_ptr<Logger> _logger;
|
||||
std::shared_ptr<WatchItems> _watchItems;
|
||||
std::shared_ptr<Enricher> _enricher;
|
||||
std::shared_ptr<RateLimiter> _rateLimiter;
|
||||
SantaCache<SantaVnode, NSString *> _certHashCache;
|
||||
}
|
||||
|
||||
@@ -211,7 +219,7 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
|
||||
decisionCache:(SNTDecisionCache *)decisionCache {
|
||||
self = [super initWithESAPI:std::move(esApi)
|
||||
metrics:std::move(metrics)
|
||||
metrics:metrics
|
||||
processor:santa::santad::Processor::kFileAccessAuthorizer];
|
||||
if (self) {
|
||||
_watchItems = std::move(watchItems);
|
||||
@@ -220,15 +228,23 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
|
||||
_decisionCache = decisionCache;
|
||||
|
||||
_famEnabled = [[SNTMetricSet sharedInstance]
|
||||
_rateLimiter = RateLimiter::Create(metrics, santa::santad::Processor::kFileAccessAuthorizer,
|
||||
kDefaultRateLimitQPS);
|
||||
|
||||
SNTMetricBooleanGauge *famEnabled = [[SNTMetricSet sharedInstance]
|
||||
booleanGaugeWithName:@"/santa/fam_enabled"
|
||||
fieldNames:@[]
|
||||
helpText:@"Whether or not the FAM client is enabled"];
|
||||
|
||||
WEAKIFY(self);
|
||||
[[SNTMetricSet sharedInstance] registerCallback:^{
|
||||
STRONGIFY(self);
|
||||
[famEnabled set:self.isSubscribed forFieldValues:@[]];
|
||||
}];
|
||||
|
||||
[self establishClientOrDie];
|
||||
|
||||
[super enableTargetPathWatching];
|
||||
[super unmuteEverything];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -413,19 +429,35 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
return specialCase;
|
||||
}
|
||||
|
||||
FileAccessPolicyDecision decision = FileAccessPolicyDecision::kDenied;
|
||||
|
||||
for (const WatchItemPolicy::Process &process : policy->processes) {
|
||||
if ([self policyProcess:process matchesESProcess:msg->process]) {
|
||||
return FileAccessPolicyDecision::kAllowed;
|
||||
decision = FileAccessPolicyDecision::kAllowed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (policy->audit_only) {
|
||||
return FileAccessPolicyDecision::kAllowedAuditOnly;
|
||||
} else {
|
||||
// TODO(xyz): Write to TTY like in exec controller?
|
||||
// TODO(xyz): Need new config item for custom message in UI
|
||||
return FileAccessPolicyDecision::kDenied;
|
||||
// If the `invert_process_exceptions` option is set, the decision should be
|
||||
// inverted from allowed to denied or vice versa. Note that this inversion
|
||||
// must be made prior to checking the policy's audit-only flag.
|
||||
if (policy->invert_process_exceptions) {
|
||||
if (decision == FileAccessPolicyDecision::kAllowed) {
|
||||
decision = FileAccessPolicyDecision::kDenied;
|
||||
} else {
|
||||
decision = FileAccessPolicyDecision::kAllowed;
|
||||
}
|
||||
}
|
||||
|
||||
if (decision == FileAccessPolicyDecision::kDenied && policy->audit_only) {
|
||||
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;
|
||||
}
|
||||
|
||||
- (FileAccessPolicyDecision)handleMessage:(const Message &)msg
|
||||
@@ -437,8 +469,10 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
forTarget:target
|
||||
toMessage:msg];
|
||||
|
||||
if (ShouldLogDecision(policyDecision)) {
|
||||
if (optionalPolicy.has_value()) {
|
||||
// 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::string policyVersionCopy = policyVersion;
|
||||
std::string targetPathCopy = target.path;
|
||||
@@ -450,10 +484,27 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
self->_enricher->Enrich(*esMsg->process, EnrichOptions::kLocalOnly),
|
||||
targetPathCopy, policyDecision);
|
||||
}];
|
||||
}
|
||||
|
||||
} else {
|
||||
LOGE(@"Unexpectedly missing policy: Unable to log file access event: %s -> %s",
|
||||
Path(msg->process->executable).data(), target.path.c_str());
|
||||
if (!optionalPolicy.value()->silent && self.fileAccessBlockCallback) {
|
||||
SNTCachedDecision *cd =
|
||||
[self.decisionCache cachedDecisionForFile:msg->process->executable->stat];
|
||||
|
||||
SNTFileAccessEvent *event = [[SNTFileAccessEvent alloc] init];
|
||||
|
||||
event.accessedPath = StringToNSString(target.path);
|
||||
event.ruleVersion = StringToNSString(optionalPolicy.value()->version);
|
||||
event.ruleName = StringToNSString(optionalPolicy.value()->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());
|
||||
|
||||
self.fileAccessBlockCallback(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,7 +582,6 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
if (!self.isSubscribed) {
|
||||
if ([super subscribe:events]) {
|
||||
self.isSubscribed = true;
|
||||
[self.famEnabled set:YES forFieldValues:@[]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,9 +593,8 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
if (self.isSubscribed) {
|
||||
if ([super unsubscribeAll]) {
|
||||
self.isSubscribed = false;
|
||||
[self.famEnabled set:NO forFieldValues:@[]];
|
||||
}
|
||||
[super unmuteEverything];
|
||||
[super unmuteAllTargetPaths];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ extern es_auth_result_t CombinePolicyResults(es_auth_result_t result1, es_auth_r
|
||||
void SetExpectationsForFileAccessAuthorizerInit(
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi) {
|
||||
EXPECT_CALL(*mockESApi, InvertTargetPathMuting).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllPaths).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).WillOnce(testing::Return(true));
|
||||
}
|
||||
|
||||
@@ -532,8 +531,9 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
|
||||
// If no policy exists, the operation is allowed
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertEqual([accessClient applyPolicy:std::nullopt forTarget:target toMessage:msg],
|
||||
XCTAssertEqual([accessClient applyPolicy:std::nullopt
|
||||
forTarget:target
|
||||
toMessage:Message(mockESApi, &esMsg)],
|
||||
FileAccessPolicyDecision::kNoPolicy);
|
||||
}
|
||||
|
||||
@@ -546,8 +546,9 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
{
|
||||
OCMExpect([self.mockConfigurator enableBadSignatureProtection]).andReturn(YES);
|
||||
esMsg.process->codesigning_flags = CS_SIGNED;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy forTarget:target toMessage:msg],
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:Message(mockESApi, &esMsg)],
|
||||
FileAccessPolicyDecision::kDeniedInvalidSignature);
|
||||
}
|
||||
|
||||
@@ -557,11 +558,12 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
{
|
||||
OCMExpect([self.mockConfigurator enableBadSignatureProtection]).andReturn(NO);
|
||||
esMsg.process->codesigning_flags = CS_SIGNED;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
OCMExpect([accessClientMock policyProcess:policyProc matchesESProcess:&esProc])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(true);
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy forTarget:target toMessage:msg],
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:Message(mockESApi, &esMsg)],
|
||||
FileAccessPolicyDecision::kAllowed);
|
||||
}
|
||||
|
||||
@@ -574,8 +576,9 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(false);
|
||||
policy->audit_only = false;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy forTarget:target toMessage:msg],
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:Message(mockESApi, &esMsg)],
|
||||
FileAccessPolicyDecision::kDenied);
|
||||
}
|
||||
|
||||
@@ -585,8 +588,50 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(false);
|
||||
policy->audit_only = true;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy forTarget:target toMessage:msg],
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:Message(mockESApi, &esMsg)],
|
||||
FileAccessPolicyDecision::kAllowedAuditOnly);
|
||||
}
|
||||
|
||||
// The remainder of the tests set the policy's `invert_process_exceptions` option
|
||||
policy->invert_process_exceptions = true;
|
||||
|
||||
// If no exceptions for inverted policy, operations are allowed
|
||||
{
|
||||
OCMExpect([accessClientMock policyProcess:policyProc matchesESProcess:&esProc])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(false);
|
||||
policy->audit_only = false;
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:Message(mockESApi, &esMsg)],
|
||||
FileAccessPolicyDecision::kAllowed);
|
||||
}
|
||||
|
||||
// For audit only policies with no exception matches and inverted exceptions, operations are
|
||||
// allowed
|
||||
{
|
||||
OCMExpect([accessClientMock policyProcess:policyProc matchesESProcess:&esProc])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(false);
|
||||
policy->audit_only = true;
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:Message(mockESApi, &esMsg)],
|
||||
FileAccessPolicyDecision::kAllowed);
|
||||
}
|
||||
|
||||
// For audit only policies with exception match and inverted exceptions, operations are allowed
|
||||
// audit only
|
||||
{
|
||||
OCMExpect([accessClientMock policyProcess:policyProc matchesESProcess:&esProc])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(true);
|
||||
policy->audit_only = true;
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:Message(mockESApi, &esMsg)],
|
||||
FileAccessPolicyDecision::kAllowedAuditOnly);
|
||||
}
|
||||
|
||||
@@ -636,7 +681,6 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
decisionCache:nil];
|
||||
|
||||
EXPECT_CALL(*mockESApi, UnsubscribeAll);
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllPaths).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).WillOnce(testing::Return(true));
|
||||
|
||||
accessClient.isSubscribed = true;
|
||||
|
||||
@@ -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,7 +87,7 @@ 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:
|
||||
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) {
|
||||
@@ -95,7 +99,23 @@ 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;
|
||||
}
|
||||
|
||||
@@ -110,11 +130,11 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
|
||||
// Enrich the message inline with the ES handler block to capture enrichment
|
||||
// data as close to the source event as possible.
|
||||
std::shared_ptr<EnrichedMessage> sharedEnrichedMessage = _enricher->Enrich(std::move(esMsg));
|
||||
std::unique_ptr<EnrichedMessage> enrichedMessage = _enricher->Enrich(std::move(esMsg));
|
||||
|
||||
// Asynchronously log the message
|
||||
[self processEnrichedMessage:std::move(sharedEnrichedMessage)
|
||||
handler:^(std::shared_ptr<EnrichedMessage> msg) {
|
||||
[self processEnrichedMessage:std::move(enrichedMessage)
|
||||
handler:^(std::unique_ptr<EnrichedMessage> msg) {
|
||||
self->_logger->Log(std::move(msg));
|
||||
recordEventMetrics(EventDisposition::kProcessed);
|
||||
}];
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#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"
|
||||
@@ -48,7 +49,7 @@ using santa::santad::logs::endpoint_security::Logger;
|
||||
|
||||
class MockEnricher : public Enricher {
|
||||
public:
|
||||
MOCK_METHOD(std::shared_ptr<EnrichedMessage>, Enrich, (Message &&));
|
||||
MOCK_METHOD(std::unique_ptr<EnrichedMessage>, Enrich, (Message &&));
|
||||
};
|
||||
|
||||
class MockAuthResultCache : public AuthResultCache {
|
||||
@@ -62,14 +63,25 @@ class MockLogger : public Logger {
|
||||
public:
|
||||
using Logger::Logger;
|
||||
|
||||
MOCK_METHOD(void, Log, (std::shared_ptr<EnrichedMessage>));
|
||||
MOCK_METHOD(void, Log, (std::unique_ptr<EnrichedMessage>));
|
||||
};
|
||||
|
||||
@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{
|
||||
@@ -94,19 +106,21 @@ class MockLogger : public Logger {
|
||||
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");
|
||||
es_file_t targetFileMatchesRegex = MakeESFile("/foo/matches");
|
||||
es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
std::shared_ptr<EnrichedMessage> enrichedMsg = std::shared_ptr<EnrichedMessage>(nullptr);
|
||||
std::unique_ptr<EnrichedMessage> enrichedMsg = std::unique_ptr<EnrichedMessage>(nullptr);
|
||||
|
||||
auto mockEnricher = std::make_shared<MockEnricher>();
|
||||
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(enrichedMsg));
|
||||
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(std::move(enrichedMsg)));
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFile)).Times(1);
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMatchesRegex)).Times(1);
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMissesRegex)).Times(1);
|
||||
|
||||
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
|
||||
|
||||
@@ -145,11 +159,11 @@ class MockLogger : public Logger {
|
||||
}]);
|
||||
}
|
||||
|
||||
// CLOSE modified, remove from cache
|
||||
// CLOSE modified, remove from cache, and matches fileChangesRegex
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg.event.close.modified = true;
|
||||
esMsg.event.close.target = &targetFile;
|
||||
esMsg.event.close.target = &targetFileMatchesRegex;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
@@ -164,10 +178,22 @@ class MockLogger : public Logger {
|
||||
XCTAssertSemaTrue(sema, 5, "Log wasn't called within expected time window");
|
||||
}
|
||||
|
||||
// CLOSE modified, remove from cache, but doesn't match fileChangesRegex
|
||||
{
|
||||
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");
|
||||
}]);
|
||||
}
|
||||
|
||||
// LINK, Prefix match, bail early
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_LINK;
|
||||
esMsg.event.link.source = &targetFile;
|
||||
esMsg.event.link.source = &targetFileMatchesRegex;
|
||||
prefixTree->InsertPrefix(esMsg.event.link.source->path.data, Unit{});
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
|
||||
@@ -111,7 +111,6 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
|
||||
|
||||
- (void)enable {
|
||||
[super enableTargetPathWatching];
|
||||
[super unmuteEverything];
|
||||
|
||||
// Get the set of protected paths
|
||||
std::set<std::string> protectedPaths = [SNTEndpointSecurityTamperResistance getProtectedPaths];
|
||||
|
||||
@@ -65,7 +65,6 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
|
||||
|
||||
// Setup mocks to handle inverting target path muting
|
||||
EXPECT_CALL(*mockESApi, InvertTargetPathMuting).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllPaths).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).WillOnce(testing::Return(true));
|
||||
|
||||
// Setup mocks to handle muting the rules db and events db
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
// Forward declarations
|
||||
@class SNTStoredEvent;
|
||||
@@ -39,8 +40,8 @@ class Logger {
|
||||
public:
|
||||
static std::unique_ptr<Logger> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
SNTEventLogType log_type, NSString *event_log_path, NSString *spool_log_path,
|
||||
size_t spool_dir_size_threshold, size_t spool_file_size_threshold,
|
||||
SNTEventLogType log_type, SNTDecisionCache *decision_cache, NSString *event_log_path,
|
||||
NSString *spool_log_path, size_t spool_dir_size_threshold, size_t spool_file_size_threshold,
|
||||
uint64_t spool_flush_timeout_ms);
|
||||
|
||||
Logger(std::shared_ptr<serializers::Serializer> serializer,
|
||||
@@ -49,7 +50,7 @@ class Logger {
|
||||
virtual ~Logger() = default;
|
||||
|
||||
virtual void Log(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage> msg);
|
||||
std::unique_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage> msg);
|
||||
|
||||
void LogAllowlist(const santa::santad::event_providers::endpoint_security::Message &msg,
|
||||
const std::string_view hash);
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/File.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Null.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Spool.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Syslog.h"
|
||||
#include "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
@@ -48,26 +50,33 @@ static const size_t kMaxExpectedWriteSizeBytes = 4096;
|
||||
|
||||
// Translate configured log type to appropriate Serializer/Writer pairs
|
||||
std::unique_ptr<Logger> Logger::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTEventLogType log_type, NSString *event_log_path,
|
||||
NSString *spool_log_path, size_t spool_dir_size_threshold,
|
||||
SNTEventLogType log_type, SNTDecisionCache *decision_cache,
|
||||
NSString *event_log_path, NSString *spool_log_path,
|
||||
size_t spool_dir_size_threshold,
|
||||
size_t spool_file_size_threshold,
|
||||
uint64_t spool_flush_timeout_ms) {
|
||||
switch (log_type) {
|
||||
case SNTEventLogTypeFilelog:
|
||||
return std::make_unique<Logger>(
|
||||
BasicString::Create(esapi),
|
||||
BasicString::Create(esapi, std::move(decision_cache)),
|
||||
File::Create(event_log_path, kFlushBufferTimeoutMS, kBufferBatchSizeBytes,
|
||||
kMaxExpectedWriteSizeBytes));
|
||||
case SNTEventLogTypeSyslog:
|
||||
return std::make_unique<Logger>(BasicString::Create(esapi, false), Syslog::Create());
|
||||
return std::make_unique<Logger>(BasicString::Create(esapi, std::move(decision_cache), false),
|
||||
Syslog::Create());
|
||||
case SNTEventLogTypeNull: return std::make_unique<Logger>(Empty::Create(), Null::Create());
|
||||
case SNTEventLogTypeProtobuf:
|
||||
LOGW(@"The EventLogType value protobuf is currently in beta. The protobuf schema is subject "
|
||||
@"to change.");
|
||||
return std::make_unique<Logger>(
|
||||
Protobuf::Create(esapi),
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -76,7 +85,7 @@ Logger::Logger(std::shared_ptr<serializers::Serializer> serializer,
|
||||
std::shared_ptr<writers::Writer> writer)
|
||||
: serializer_(std::move(serializer)), writer_(std::move(writer)) {}
|
||||
|
||||
void Logger::Log(std::shared_ptr<EnrichedMessage> msg) {
|
||||
void Logger::Log(std::unique_ptr<EnrichedMessage> msg) {
|
||||
writer_->Write(serializer_->SerializeMessage(std::move(msg)));
|
||||
}
|
||||
|
||||
|
||||
@@ -103,28 +103,33 @@ class MockWriter : public Null {
|
||||
// Ensure that the factory method creates expected serializers/writers pairs
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
XCTAssertEqual(nullptr, Logger::Create(mockESApi, (SNTEventLogType)123, @"/tmp/temppy",
|
||||
XCTAssertEqual(nullptr, Logger::Create(mockESApi, (SNTEventLogType)123, nil, @"/tmp/temppy",
|
||||
@"/tmp/spool", 1, 1, 1));
|
||||
|
||||
LoggerPeer logger(
|
||||
Logger::Create(mockESApi, SNTEventLogTypeFilelog, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
Logger::Create(mockESApi, SNTEventLogTypeFilelog, nil, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<BasicString>(logger.Serializer()));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<File>(logger.Writer()));
|
||||
|
||||
logger = LoggerPeer(
|
||||
Logger::Create(mockESApi, SNTEventLogTypeSyslog, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
Logger::Create(mockESApi, SNTEventLogTypeSyslog, nil, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<BasicString>(logger.Serializer()));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Syslog>(logger.Writer()));
|
||||
|
||||
logger = LoggerPeer(
|
||||
Logger::Create(mockESApi, SNTEventLogTypeNull, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
Logger::Create(mockESApi, SNTEventLogTypeNull, nil, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Empty>(logger.Serializer()));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Null>(logger.Writer()));
|
||||
|
||||
logger = LoggerPeer(
|
||||
Logger::Create(mockESApi, SNTEventLogTypeProtobuf, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
logger = LoggerPeer(Logger::Create(mockESApi, SNTEventLogTypeProtobuf, nil, @"/tmp/temppy",
|
||||
@"/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 {
|
||||
@@ -136,16 +141,19 @@ class MockWriter : public Null {
|
||||
es_message_t msg;
|
||||
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
auto enrichedMsg = std::make_shared<EnrichedMessage>(
|
||||
EnrichedClose(Message(mockESApi, &msg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
|
||||
EXPECT_CALL(*mockSerializer, SerializeMessage(testing::A<const EnrichedClose &>())).Times(1);
|
||||
EXPECT_CALL(*mockWriter, Write).Times(1);
|
||||
{
|
||||
auto enrichedMsg = std::make_unique<EnrichedMessage>(
|
||||
EnrichedClose(Message(mockESApi, &msg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
|
||||
Logger(mockSerializer, mockWriter).Log(enrichedMsg);
|
||||
EXPECT_CALL(*mockSerializer, SerializeMessage(testing::A<const EnrichedClose &>())).Times(1);
|
||||
EXPECT_CALL(*mockWriter, Write).Times(1);
|
||||
|
||||
Logger(mockSerializer, mockWriter).Log(std::move(enrichedMsg));
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
@@ -31,11 +32,11 @@ class BasicString : public Serializer {
|
||||
public:
|
||||
static std::shared_ptr<BasicString> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
bool prefix_time_name = true);
|
||||
SNTDecisionCache *decision_cache, bool prefix_time_name = true);
|
||||
|
||||
BasicString(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
bool prefix_time_name);
|
||||
SNTDecisionCache *decision_cache, bool prefix_time_name);
|
||||
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
#include <string>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h"
|
||||
@@ -92,11 +91,13 @@ std::string GetReasonString(SNTEventState event_state) {
|
||||
case SNTEventStateAllowCertificate: return "CERT";
|
||||
case SNTEventStateAllowScope: return "SCOPE";
|
||||
case SNTEventStateAllowTeamID: return "TEAMID";
|
||||
case SNTEventStateAllowSigningID: return "SIGNINGID";
|
||||
case SNTEventStateAllowUnknown: return "UNKNOWN";
|
||||
case SNTEventStateBlockBinary: return "BINARY";
|
||||
case SNTEventStateBlockCertificate: return "CERT";
|
||||
case SNTEventStateBlockScope: return "SCOPE";
|
||||
case SNTEventStateBlockTeamID: return "TEAMID";
|
||||
case SNTEventStateBlockSigningID: return "SIGNINGID";
|
||||
case SNTEventStateBlockLongPath: return "LONG_PATH";
|
||||
case SNTEventStateBlockUnknown: return "UNKNOWN";
|
||||
default: return "NOTRUNNING";
|
||||
@@ -177,12 +178,14 @@ static char *FormattedDateString(char *buf, size_t len) {
|
||||
}
|
||||
|
||||
std::shared_ptr<BasicString> BasicString::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTDecisionCache *decision_cache,
|
||||
bool prefix_time_name) {
|
||||
return std::make_shared<BasicString>(esapi, prefix_time_name);
|
||||
return std::make_shared<BasicString>(esapi, decision_cache, prefix_time_name);
|
||||
}
|
||||
|
||||
BasicString::BasicString(std::shared_ptr<EndpointSecurityAPI> esapi, bool prefix_time_name)
|
||||
: esapi_(esapi), prefix_time_name_(prefix_time_name) {}
|
||||
BasicString::BasicString(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTDecisionCache *decision_cache, bool prefix_time_name)
|
||||
: Serializer(std::move(decision_cache)), esapi_(esapi), prefix_time_name_(prefix_time_name) {}
|
||||
|
||||
std::string BasicString::CreateDefaultString(size_t reserved_size) {
|
||||
std::string str;
|
||||
@@ -245,9 +248,6 @@ std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg, SNTC
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString(1024); // EXECs tend to be bigger, reserve more space.
|
||||
|
||||
// Only need to grab the shared instance once
|
||||
static SNTConfigurator *configurator = [SNTConfigurator configurator];
|
||||
|
||||
str.append("action=EXEC|decision=");
|
||||
str.append(GetDecisionString(cd.decision));
|
||||
str.append("|reason=");
|
||||
@@ -291,7 +291,7 @@ std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg, SNTC
|
||||
msg.instigator().real_group());
|
||||
|
||||
str.append("|mode=");
|
||||
str.append(GetModeString([configurator clientMode]));
|
||||
str.append(GetModeString(cd.decisionClientMode));
|
||||
str.append("|path=");
|
||||
str.append(FilePath(esm.event.exec.target->executable).Sanitized());
|
||||
|
||||
|
||||
@@ -56,10 +56,10 @@ using santa::santad::logs::endpoint_security::serializers::GetModeString;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetReasonString;
|
||||
|
||||
std::string BasicStringSerializeMessage(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
es_message_t *esMsg, SNTDecisionCache *decisionCache) {
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
std::shared_ptr<Serializer> bs = BasicString::Create(mockESApi, false);
|
||||
std::shared_ptr<Serializer> bs = BasicString::Create(mockESApi, decisionCache, false);
|
||||
std::vector<uint8_t> ret = bs->SerializeMessage(Enricher().Enrich(Message(mockESApi, esMsg)));
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
@@ -69,7 +69,7 @@ std::string BasicStringSerializeMessage(std::shared_ptr<MockEndpointSecurityAPI>
|
||||
|
||||
std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
return BasicStringSerializeMessage(mockESApi, esMsg);
|
||||
return BasicStringSerializeMessage(mockESApi, esMsg, nil);
|
||||
}
|
||||
|
||||
@interface BasicStringTest : XCTestCase
|
||||
@@ -94,6 +94,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
self.testCachedDecision.sha256 = @"1234_hash";
|
||||
self.testCachedDecision.quarantineURL = @"google.com";
|
||||
self.testCachedDecision.certSHA256 = @"5678_hash";
|
||||
self.testCachedDecision.decisionClientMode = SNTClientModeLockdown;
|
||||
|
||||
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
|
||||
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
|
||||
@@ -163,7 +164,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
.WillOnce(testing::Return(es_string_token_t{5, "-l\n-t"}))
|
||||
.WillOnce(testing::Return(es_string_token_t{8, "-v\r--foo"}));
|
||||
|
||||
std::string got = BasicStringSerializeMessage(mockESApi, &esMsg);
|
||||
std::string got = BasicStringSerializeMessage(mockESApi, &esMsg, self.mockDecisionCache);
|
||||
std::string want =
|
||||
"action=EXEC|decision=ALLOW|reason=BINARY|explain=extra!|sha256=1234_hash|"
|
||||
"cert_sha256=5678_hash|cert_cn=|quarantine_url=google.com|pid=12|pidversion="
|
||||
@@ -289,7 +290,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
std::vector<uint8_t> ret =
|
||||
BasicString::Create(nullptr, false)
|
||||
BasicString::Create(nullptr, nil, false)
|
||||
->SerializeFileAccess("v1.0", "pol_name", Message(mockESApi, &esMsg),
|
||||
Enricher().Enrich(*esMsg.process), "file_target",
|
||||
FileAccessPolicyDecision::kAllowedAuditOnly);
|
||||
@@ -310,7 +311,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(mockESApi, false)
|
||||
std::vector<uint8_t> ret = BasicString::Create(mockESApi, nil, false)
|
||||
->SerializeAllowlist(Message(mockESApi, &esMsg), "test_hash");
|
||||
|
||||
XCTAssertTrue(testing::Mock::VerifyAndClearExpectations(mockESApi.get()),
|
||||
@@ -333,7 +334,8 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
se.fileBundlePath = @"file_bundle_path";
|
||||
se.filePath = @"file_path";
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeBundleHashingEvent(se);
|
||||
std::vector<uint8_t> ret =
|
||||
BasicString::Create(nullptr, nil, false)->SerializeBundleHashingEvent(se);
|
||||
std::string got(ret.begin(), ret.end());
|
||||
|
||||
std::string want = "action=BUNDLE|sha256=file_hash"
|
||||
@@ -360,7 +362,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator enableMachineIDDecoration]).andReturn(NO);
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeDiskAppeared(props);
|
||||
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"
|
||||
@@ -376,7 +378,8 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
@"DAMediaBSDName" : @"bsd",
|
||||
};
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeDiskDisappeared(props);
|
||||
std::vector<uint8_t> ret =
|
||||
BasicString::Create(nullptr, nil, false)->SerializeDiskDisappeared(props);
|
||||
std::string got(ret.begin(), ret.end());
|
||||
|
||||
std::string want = "action=DISKDISAPPEAR|mount=path|volume=|bsdname=bsd|machineid=my_id\n";
|
||||
@@ -418,6 +421,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
{SNTEventStateBlockCertificate, "CERT"},
|
||||
{SNTEventStateBlockScope, "SCOPE"},
|
||||
{SNTEventStateBlockTeamID, "TEAMID"},
|
||||
{SNTEventStateBlockSigningID, "SIGNINGID"},
|
||||
{SNTEventStateBlockLongPath, "LONG_PATH"},
|
||||
{SNTEventStateAllowUnknown, "UNKNOWN"},
|
||||
{SNTEventStateAllowBinary, "BINARY"},
|
||||
@@ -427,6 +431,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
{SNTEventStateAllowTransitive, "TRANSITIVE"},
|
||||
{SNTEventStateAllowPendingTransitive, "PENDING_TRANSITIVE"},
|
||||
{SNTEventStateAllowTeamID, "TEAMID"},
|
||||
{SNTEventStateAllowSigningID, "SIGNINGID"},
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToReason) {
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace santa::santad::logs::endpoint_security::serializers {
|
||||
class Empty : public Serializer {
|
||||
public:
|
||||
static std::shared_ptr<Empty> Create();
|
||||
Empty();
|
||||
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
|
||||
|
||||
@@ -31,6 +31,8 @@ std::shared_ptr<Empty> Empty::Create() {
|
||||
return std::make_shared<Empty>();
|
||||
}
|
||||
|
||||
Empty::Empty() : Serializer(nil) {}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedClose &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -25,16 +25,19 @@
|
||||
#include "Source/common/santa_proto_include_wrapper.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
class Protobuf : public Serializer {
|
||||
public:
|
||||
static std::shared_ptr<Protobuf> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi);
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
SNTDecisionCache *decision_cache, bool json = false);
|
||||
|
||||
Protobuf(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi);
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
SNTDecisionCache *decision_cache, bool json = false);
|
||||
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
|
||||
@@ -84,6 +87,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,19 +17,22 @@
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <google/protobuf/stubs/status.h>
|
||||
#include <google/protobuf/util/json_util.h>
|
||||
#include <mach/message.h>
|
||||
#include <math.h>
|
||||
#include <sys/proc_info.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#include "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/common/String.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
@@ -37,7 +40,10 @@
|
||||
|
||||
using google::protobuf::Arena;
|
||||
using google::protobuf::Timestamp;
|
||||
using google::protobuf::util::JsonPrintOptions;
|
||||
using google::protobuf::util::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::EnrichedEventType;
|
||||
@@ -63,11 +69,14 @@ namespace pbv1 = ::santa::pb::v1;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
std::shared_ptr<Protobuf> Protobuf::Create(std::shared_ptr<EndpointSecurityAPI> esapi) {
|
||||
return std::make_shared<Protobuf>(esapi);
|
||||
std::shared_ptr<Protobuf> Protobuf::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTDecisionCache *decision_cache, bool json) {
|
||||
return std::make_shared<Protobuf>(esapi, std::move(decision_cache), json);
|
||||
}
|
||||
|
||||
Protobuf::Protobuf(std::shared_ptr<EndpointSecurityAPI> esapi) : 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);
|
||||
@@ -94,15 +103,15 @@ static inline void EncodePath(std::string *buf, const es_file_t *es_file) {
|
||||
buf->append(std::string_view(es_file->path.data, es_file->path.length));
|
||||
}
|
||||
|
||||
static inline void EncodeString(std::string *buf, NSString *value) {
|
||||
static inline void EncodeString(std::function<std::string *()> lazy_f, NSString *value) {
|
||||
if (value) {
|
||||
buf->append(std::string_view([value UTF8String], [value length]));
|
||||
lazy_f()->append(NSStringToUTF8StringView(value));
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeString(std::string *buf, std::string_view value) {
|
||||
static inline void EncodeString(std::function<std::string *()> lazy_f, std::string_view value) {
|
||||
if (value.length() > 0) {
|
||||
buf->append(std::string_view(value.data(), value.length()));
|
||||
lazy_f()->append(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +134,7 @@ static inline void EncodeGroupInfo(::pbv1::GroupInfo *pb_group_info, gid_t gid,
|
||||
static inline void EncodeHash(::pbv1::Hash *pb_hash, NSString *sha256) {
|
||||
if (sha256) {
|
||||
pb_hash->set_type(::pbv1::Hash::HASH_ALGO_SHA256);
|
||||
pb_hash->set_hash([sha256 UTF8String], [sha256 length]);
|
||||
EncodeString([pb_hash] { return pb_hash->mutable_hash(); }, sha256);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +171,7 @@ static inline void EncodeFileInfo(::pbv1::FileInfo *pb_file, const es_file_t *es
|
||||
|
||||
static inline void EncodeFileInfoLight(::pbv1::FileInfoLight *pb_file, std::string_view path,
|
||||
bool truncated) {
|
||||
EncodeString(pb_file->mutable_path(), path);
|
||||
EncodeString([pb_file] { return pb_file->mutable_path(); }, path);
|
||||
pb_file->set_truncated(truncated);
|
||||
}
|
||||
|
||||
@@ -262,9 +271,7 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
EncodeHash(pb_cert_info->mutable_hash(), cert_hash);
|
||||
}
|
||||
|
||||
if (common_name) {
|
||||
pb_cert_info->set_common_name([common_name UTF8String], [common_name length]);
|
||||
}
|
||||
EncodeString([pb_cert_info] { return pb_cert_info->mutable_common_name(); }, common_name);
|
||||
}
|
||||
|
||||
::pbv1::Execution::Decision GetDecisionEnum(SNTEventState event_state) {
|
||||
@@ -286,11 +293,13 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
case SNTEventStateAllowCertificate: return ::pbv1::Execution::REASON_CERT;
|
||||
case SNTEventStateAllowScope: return ::pbv1::Execution::REASON_SCOPE;
|
||||
case SNTEventStateAllowTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
|
||||
case SNTEventStateAllowSigningID: return ::pbv1::Execution::REASON_SIGNING_ID;
|
||||
case SNTEventStateAllowUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
|
||||
case SNTEventStateBlockBinary: return ::pbv1::Execution::REASON_BINARY;
|
||||
case SNTEventStateBlockCertificate: return ::pbv1::Execution::REASON_CERT;
|
||||
case SNTEventStateBlockScope: return ::pbv1::Execution::REASON_SCOPE;
|
||||
case SNTEventStateBlockTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
|
||||
case SNTEventStateBlockSigningID: return ::pbv1::Execution::REASON_SIGNING_ID;
|
||||
case SNTEventStateBlockLongPath: return ::pbv1::Execution::REASON_LONG_PATH;
|
||||
case SNTEventStateBlockUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
|
||||
default: return ::pbv1::Execution::REASON_NOT_RUNNING;
|
||||
@@ -356,7 +365,7 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
::pbv1::SantaMessage *santa_msg = Arena::CreateMessage<::pbv1::SantaMessage>(arena);
|
||||
|
||||
if (EnabledMachineID()) {
|
||||
EncodeString(santa_msg->mutable_machine_id(), MachineID());
|
||||
EncodeString([santa_msg] { return santa_msg->mutable_machine_id(); }, MachineID());
|
||||
}
|
||||
EncodeTimestamp(santa_msg->mutable_event_time(), event_time);
|
||||
EncodeTimestamp(santa_msg->mutable_processed_time(), processed_time);
|
||||
@@ -383,6 +392,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;
|
||||
|
||||
google::protobuf::util::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;
|
||||
@@ -422,9 +451,6 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCach
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
// Only need to grab the shared instance once
|
||||
static SNTConfigurator *configurator = [SNTConfigurator configurator];
|
||||
|
||||
GetDecisionEnum(cd.decision);
|
||||
|
||||
::pbv1::Execution *pb_exec = santa_msg->mutable_execution();
|
||||
@@ -485,24 +511,17 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCach
|
||||
|
||||
pb_exec->set_decision(GetDecisionEnum(cd.decision));
|
||||
pb_exec->set_reason(GetReasonEnum(cd.decision));
|
||||
pb_exec->set_mode(GetModeEnum([configurator clientMode]));
|
||||
pb_exec->set_mode(GetModeEnum(cd.decisionClientMode));
|
||||
|
||||
if (cd.certSHA256 || cd.certCommonName) {
|
||||
EncodeCertificateInfo(pb_exec->mutable_certificate_info(), cd.certSHA256, cd.certCommonName);
|
||||
}
|
||||
|
||||
if (cd.decisionExtra) {
|
||||
pb_exec->set_explain([cd.decisionExtra UTF8String], [cd.decisionExtra length]);
|
||||
}
|
||||
|
||||
if (cd.quarantineURL) {
|
||||
pb_exec->set_quarantine_url([cd.quarantineURL UTF8String], [cd.quarantineURL length]);
|
||||
}
|
||||
EncodeString([pb_exec] { return pb_exec->mutable_explain(); }, cd.decisionExtra);
|
||||
EncodeString([pb_exec] { return pb_exec->mutable_quarantine_url(); }, cd.quarantineURL);
|
||||
|
||||
NSString *orig_path = Utilities::OriginalPathForTranslocation(msg.es_msg().event.exec.target);
|
||||
if (orig_path) {
|
||||
pb_exec->set_original_path([orig_path UTF8String], [orig_path length]);
|
||||
}
|
||||
EncodeString([pb_exec] { return pb_exec->mutable_original_path(); }, orig_path);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
@@ -594,8 +613,9 @@ std::vector<uint8_t> Protobuf::SerializeFileAccess(const std::string &policy_ver
|
||||
EncodeProcessInfo(file_access->mutable_instigator(), msg->version, msg->process,
|
||||
enriched_process);
|
||||
EncodeFileInfoLight(file_access->mutable_target(), target, false);
|
||||
EncodeString(file_access->mutable_policy_version(), policy_version);
|
||||
EncodeString(file_access->mutable_policy_name(), policy_name);
|
||||
EncodeString([file_access] { return file_access->mutable_policy_version(); }, policy_version);
|
||||
EncodeString([file_access] { return file_access->mutable_policy_name(); }, policy_name);
|
||||
|
||||
file_access->set_access_type(GetAccessType(msg->event_type));
|
||||
file_access->set_policy_decision(GetPolicyDecision(decision));
|
||||
|
||||
@@ -629,10 +649,12 @@ std::vector<uint8_t> Protobuf::SerializeBundleHashingEvent(SNTStoredEvent *event
|
||||
|
||||
EncodeHash(pb_bundle->mutable_file_hash(), event.fileSHA256);
|
||||
EncodeHash(pb_bundle->mutable_bundle_hash(), event.fileBundleHash);
|
||||
pb_bundle->set_bundle_name([NonNull(event.fileBundleName) UTF8String]);
|
||||
pb_bundle->set_bundle_id([NonNull(event.fileBundleID) UTF8String]);
|
||||
pb_bundle->set_bundle_path([NonNull(event.fileBundlePath) UTF8String]);
|
||||
pb_bundle->set_path([NonNull(event.filePath) UTF8String]);
|
||||
EncodeString([pb_bundle] { return pb_bundle->mutable_bundle_name(); },
|
||||
NonNull(event.fileBundleName));
|
||||
EncodeString([pb_bundle] { return pb_bundle->mutable_bundle_id(); }, NonNull(event.fileBundleID));
|
||||
EncodeString([pb_bundle] { return pb_bundle->mutable_bundle_path(); },
|
||||
NonNull(event.fileBundlePath));
|
||||
EncodeString([pb_bundle] { return pb_bundle->mutable_path(); }, NonNull(event.filePath));
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
@@ -652,14 +674,14 @@ static void EncodeDisk(::pbv1::Disk *pb_disk, ::pbv1::Disk_Action action, NSDict
|
||||
stringWithFormat:@"%@ %@", NonNull(props[@"DADeviceVendor"]), NonNull(props[@"DADeviceModel"])];
|
||||
model = [model stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
|
||||
EncodeString(pb_disk->mutable_mount(), [props[@"DAVolumePath"] path]);
|
||||
EncodeString(pb_disk->mutable_volume(), props[@"DAVolumeName"]);
|
||||
EncodeString(pb_disk->mutable_bsd_name(), props[@"DAMediaBSDName"]);
|
||||
EncodeString(pb_disk->mutable_fs(), props[@"DAVolumeKind"]);
|
||||
EncodeString(pb_disk->mutable_model(), model);
|
||||
EncodeString(pb_disk->mutable_serial(), serial);
|
||||
EncodeString(pb_disk->mutable_bus(), props[@"DADeviceProtocol"]);
|
||||
EncodeString(pb_disk->mutable_dmg_path(), dmg_path);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_mount(); }, [props[@"DAVolumePath"] path]);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_volume(); }, props[@"DAVolumeName"]);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_bsd_name(); }, props[@"DAMediaBSDName"]);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_fs(); }, props[@"DAVolumeKind"]);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_model(); }, model);
|
||||
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);
|
||||
|
||||
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/util/json_util.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"
|
||||
@@ -46,6 +45,7 @@
|
||||
|
||||
using google::protobuf::Timestamp;
|
||||
using google::protobuf::util::JsonPrintOptions;
|
||||
using google::protobuf::util::JsonStringToMessage;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
@@ -128,12 +128,6 @@ bool CompareTime(const Timestamp ×tamp, struct timespec ts) {
|
||||
return timestamp.seconds() == ts.tv_sec && timestamp.nanos() == ts.tv_nsec;
|
||||
}
|
||||
|
||||
void CheckSantaMessage(const ::pbv1::SantaMessage &santaMsg, const es_message_t &esMsg,
|
||||
struct timespec enrichmentTime) {
|
||||
XCTAssertTrue(CompareTime(santaMsg.processed_time(), enrichmentTime));
|
||||
XCTAssertTrue(CompareTime(santaMsg.event_time(), esMsg.time));
|
||||
}
|
||||
|
||||
const google::protobuf::Message &SantaMessageEvent(const ::pbv1::SantaMessage &santaMsg) {
|
||||
switch (santaMsg.event_case()) {
|
||||
case ::pbv1::SantaMessage::kExecution: return santaMsg.execution();
|
||||
@@ -167,23 +161,34 @@ std::string ConvertMessageToJsonString(const ::pbv1::SantaMessage &santaMsg) {
|
||||
return json;
|
||||
}
|
||||
|
||||
void CheckProto(const ::pbv1::SantaMessage &santaMsg,
|
||||
std::shared_ptr<EnrichedMessage> enrichedMsg) {
|
||||
return std::visit(
|
||||
[santaMsg](const EnrichedEventType &enrichedEvent) {
|
||||
CheckSantaMessage(santaMsg, enrichedEvent.es_msg(), enrichedEvent.enrichment_time());
|
||||
NSString *wantData = LoadTestJson(EventTypeToFilename(enrichedEvent.es_msg().event_type),
|
||||
enrichedEvent.es_msg().version);
|
||||
std::string got = ConvertMessageToJsonString(santaMsg);
|
||||
NSDictionary *findDelta(NSDictionary *a, NSDictionary *b) {
|
||||
NSMutableDictionary *delta = NSMutableDictionary.dictionary;
|
||||
|
||||
XCTAssertEqualObjects([NSString stringWithUTF8String:got.c_str()], wantData);
|
||||
},
|
||||
enrichedMsg->GetEnrichedMessage());
|
||||
// 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 *)) {
|
||||
es_message_t *),
|
||||
SNTDecisionCache *decisionCache, bool json = false) {
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
for (uint32_t cur_version = 1; cur_version <= MaxSupportedESMessageVersionForCurrentOS();
|
||||
@@ -204,16 +209,62 @@ void SerializeAndCheck(es_event_type_t eventType,
|
||||
|
||||
messageSetup(mockESApi, &esMsg);
|
||||
|
||||
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi);
|
||||
std::shared_ptr<EnrichedMessage> enrichedMsg = Enricher().Enrich(Message(mockESApi, &esMsg));
|
||||
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi, decisionCache, json);
|
||||
std::unique_ptr<EnrichedMessage> enrichedMsg = Enricher().Enrich(Message(mockESApi, &esMsg));
|
||||
|
||||
std::vector<uint8_t> vec = bs->SerializeMessage(enrichedMsg);
|
||||
// Copy some values we need to check later before the object is moved out of this funciton
|
||||
struct timespec enrichmentTime;
|
||||
struct timespec msgTime;
|
||||
NSString *wantData = std::visit(
|
||||
[&msgTime, &enrichmentTime](const EnrichedEventType &enrichedEvent) {
|
||||
msgTime = enrichedEvent.es_msg().time;
|
||||
enrichmentTime = enrichedEvent.enrichment_time();
|
||||
|
||||
return LoadTestJson(EventTypeToFilename(enrichedEvent.es_msg().event_type),
|
||||
enrichedEvent.es_msg().version);
|
||||
},
|
||||
enrichedMsg->GetEnrichedMessage());
|
||||
|
||||
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;
|
||||
|
||||
CheckProto(santaMsg, enrichedMsg);
|
||||
if (json) {
|
||||
// Parse the jsonified string into the protobuf
|
||||
// gotData = protoStr;
|
||||
google::protobuf::util::JsonParseOptions options;
|
||||
options.ignore_unknown_fields = true;
|
||||
google::protobuf::util::Status status = JsonStringToMessage(protoStr, &santaMsg, options);
|
||||
gotData = ConvertMessageToJsonString(santaMsg);
|
||||
} else {
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
gotData = ConvertMessageToJsonString(santaMsg);
|
||||
}
|
||||
|
||||
XCTAssertTrue(CompareTime(santaMsg.processed_time(), enrichmentTime));
|
||||
XCTAssertTrue(CompareTime(santaMsg.event_time(), msgTime));
|
||||
|
||||
// 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());
|
||||
@@ -226,7 +277,7 @@ void SerializeAndCheckNonESEvents(
|
||||
const Message &msg)) {
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi);
|
||||
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi, nil);
|
||||
|
||||
for (uint32_t cur_version = 1; cur_version <= MaxSupportedESMessageVersionForCurrentOS();
|
||||
cur_version++) {
|
||||
@@ -280,6 +331,7 @@ void SerializeAndCheckNonESEvents(
|
||||
self.testCachedDecision.sha256 = @"1234_file_hash";
|
||||
self.testCachedDecision.quarantineURL = @"google.com";
|
||||
self.testCachedDecision.certSHA256 = @"5678_cert_hash";
|
||||
self.testCachedDecision.decisionClientMode = SNTClientModeLockdown;
|
||||
|
||||
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
|
||||
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
|
||||
@@ -293,25 +345,36 @@ void SerializeAndCheckNonESEvents(
|
||||
[self.mockDecisionCache stopMocking];
|
||||
}
|
||||
|
||||
- (void)serializeAndCheckEvent:(es_event_type_t)eventType
|
||||
messageSetup:(void (^)(std::shared_ptr<MockEndpointSecurityAPI>,
|
||||
es_message_t *))messageSetup
|
||||
json:(BOOL)json {
|
||||
SerializeAndCheck(eventType, messageSetup, self.mockDecisionCache, (bool)json);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageClose {
|
||||
__block es_file_t file = MakeESFile("close_file", MakeStat(300));
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_CLOSE,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.close.modified = true;
|
||||
esMsg->event.close.target = &file;
|
||||
});
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_CLOSE
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.close.modified = true;
|
||||
esMsg->event.close.target = &file;
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExchange {
|
||||
__block es_file_t file1 = MakeESFile("exchange_file_1", MakeStat(300));
|
||||
__block es_file_t file2 = MakeESFile("exchange_file_1", MakeStat(400));
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.exchangedata.file1 = &file1;
|
||||
esMsg->event.exchangedata.file2 = &file2;
|
||||
});
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.exchangedata.file1 = &file1;
|
||||
esMsg->event.exchangedata.file2 = &file2;
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testGetDecisionEnum {
|
||||
@@ -335,7 +398,7 @@ void SerializeAndCheckNonESEvents(
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToDecision) {
|
||||
XCTAssertEqual(GetDecisionEnum(kv.first), kv.second, @"Bad decision for state: %ld", kv.first);
|
||||
XCTAssertEqual(GetDecisionEnum(kv.first), kv.second, @"Bad decision for state: %llu", kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,6 +411,7 @@ void SerializeAndCheckNonESEvents(
|
||||
{SNTEventStateBlockCertificate, ::pbv1::Execution::REASON_CERT},
|
||||
{SNTEventStateBlockScope, ::pbv1::Execution::REASON_SCOPE},
|
||||
{SNTEventStateBlockTeamID, ::pbv1::Execution::REASON_TEAM_ID},
|
||||
{SNTEventStateBlockSigningID, ::pbv1::Execution::REASON_SIGNING_ID},
|
||||
{SNTEventStateBlockLongPath, ::pbv1::Execution::REASON_LONG_PATH},
|
||||
{SNTEventStateAllowUnknown, ::pbv1::Execution::REASON_UNKNOWN},
|
||||
{SNTEventStateAllowBinary, ::pbv1::Execution::REASON_BINARY},
|
||||
@@ -357,10 +421,11 @@ void SerializeAndCheckNonESEvents(
|
||||
{SNTEventStateAllowTransitive, ::pbv1::Execution::REASON_TRANSITIVE},
|
||||
{SNTEventStateAllowPendingTransitive, ::pbv1::Execution::REASON_PENDING_TRANSITIVE},
|
||||
{SNTEventStateAllowTeamID, ::pbv1::Execution::REASON_TEAM_ID},
|
||||
{SNTEventStateAllowSigningID, ::pbv1::Execution::REASON_SIGNING_ID},
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToReason) {
|
||||
XCTAssertEqual(GetReasonEnum(kv.first), kv.second, @"Bad reason for state: %ld", kv.first);
|
||||
XCTAssertEqual(GetReasonEnum(kv.first), kv.second, @"Bad reason for state: %llu", kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,7 +438,7 @@ void SerializeAndCheckNonESEvents(
|
||||
};
|
||||
|
||||
for (const auto &kv : clientModeToExecMode) {
|
||||
XCTAssertEqual(GetModeEnum(kv.first), kv.second, @"Bad mode for state: %ld", kv.first);
|
||||
XCTAssertEqual(GetModeEnum(kv.first), kv.second, @"Bad mode for client mode: %ld", kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,45 +478,102 @@ void SerializeAndCheckNonESEvents(
|
||||
procTarget.signing_id = MakeESStringToken("my_signing_id");
|
||||
procTarget.team_id = MakeESStringToken("my_team_id");
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXEC, ^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.exec.target = &procTarget;
|
||||
esMsg->event.exec.cwd = &fileCwd;
|
||||
esMsg->event.exec.script = &fileScript;
|
||||
[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;
|
||||
}
|
||||
// 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, 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")));
|
||||
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));
|
||||
}
|
||||
});
|
||||
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: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 {
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXIT,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.exit.stat = W_EXITCODE(1, 0);
|
||||
});
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_EXIT
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.exit.stat = W_EXITCODE(1, 0);
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testEncodeExitStatus {
|
||||
@@ -484,10 +606,12 @@ void SerializeAndCheckNonESEvents(
|
||||
MakeESProcess(&procFileChild, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
procChild.tty = &ttyFileChild;
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_FORK,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.fork.child = &procChild;
|
||||
});
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_FORK
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.fork.child = &procChild;
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageLink {
|
||||
@@ -495,12 +619,14 @@ void SerializeAndCheckNonESEvents(
|
||||
__block es_file_t fileTargetDir = MakeESFile("target_dir");
|
||||
es_string_token_t targetTok = MakeESStringToken("target_file");
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_LINK,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.link.source = &fileSource;
|
||||
esMsg->event.link.target_dir = &fileTargetDir;
|
||||
esMsg->event.link.target_filename = targetTok;
|
||||
});
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_LINK
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.link.source = &fileSource;
|
||||
esMsg->event.link.target_dir = &fileTargetDir;
|
||||
esMsg->event.link.target_filename = targetTok;
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageRename {
|
||||
@@ -508,30 +634,34 @@ void SerializeAndCheckNonESEvents(
|
||||
__block es_file_t fileTargetDir = MakeESFile("target_dir");
|
||||
es_string_token_t targetTok = MakeESStringToken("target_file");
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_RENAME,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.rename.source = &fileSource;
|
||||
// Test new and existing destination types
|
||||
if (esMsg->version == 4) {
|
||||
esMsg->event.rename.destination.existing_file = &fileTargetDir;
|
||||
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
|
||||
} else {
|
||||
esMsg->event.rename.destination.new_path.dir = &fileTargetDir;
|
||||
esMsg->event.rename.destination.new_path.filename = targetTok;
|
||||
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
|
||||
}
|
||||
});
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_RENAME
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.rename.source = &fileSource;
|
||||
// Test new and existing destination types
|
||||
if (esMsg->version == 4) {
|
||||
esMsg->event.rename.destination.existing_file = &fileTargetDir;
|
||||
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
|
||||
} else {
|
||||
esMsg->event.rename.destination.new_path.dir = &fileTargetDir;
|
||||
esMsg->event.rename.destination.new_path.filename = targetTok;
|
||||
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
|
||||
}
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageUnlink {
|
||||
__block es_file_t fileTarget = MakeESFile("unlink_file", MakeStat(300));
|
||||
__block es_file_t fileTargetParent = MakeESFile("unlink_file_parent", MakeStat(400));
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_UNLINK,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.unlink.target = &fileTarget;
|
||||
esMsg->event.unlink.parent_dir = &fileTargetParent;
|
||||
});
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_UNLINK
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.unlink.target = &fileTarget;
|
||||
esMsg->event.unlink.parent_dir = &fileTargetParent;
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testGetAccessType {
|
||||
@@ -608,7 +738,7 @@ void SerializeAndCheckNonESEvents(
|
||||
se.fileBundlePath = @"file_bundle_path";
|
||||
se.filePath = @"file_path";
|
||||
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeBundleHashingEvent(se);
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr, nil)->SerializeBundleHashingEvent(se);
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
@@ -643,7 +773,7 @@ void SerializeAndCheckNonESEvents(
|
||||
@"DADeviceProtocol" : @"usb",
|
||||
};
|
||||
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeDiskAppeared(props);
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr, nil)->SerializeDiskAppeared(props);
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
@@ -680,7 +810,7 @@ void SerializeAndCheckNonESEvents(
|
||||
@"DADeviceProtocol" : @"usb",
|
||||
};
|
||||
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeDiskDisappeared(props);
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr, nil)->SerializeDiskDisappeared(props);
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
|
||||
@@ -51,8 +51,7 @@ class SanitizableString {
|
||||
friend std::ostream &operator<<(std::ostream &ss, const SanitizableString &sani_string);
|
||||
|
||||
private:
|
||||
const char *data_;
|
||||
size_t length_;
|
||||
std::string_view data_;
|
||||
mutable bool sanitized_ = false;
|
||||
mutable std::optional<std::string> sanitized_string_;
|
||||
};
|
||||
|
||||
@@ -14,37 +14,35 @@
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h"
|
||||
|
||||
#include "Source/common/String.h"
|
||||
|
||||
using santa::common::NSStringToUTF8StringView;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
SanitizableString::SanitizableString(const es_file_t *file)
|
||||
: data_(file->path.data), length_(file->path.length) {}
|
||||
: data_(file->path.data, file->path.length) {}
|
||||
|
||||
SanitizableString::SanitizableString(const es_string_token_t &tok)
|
||||
: data_(tok.data), length_(tok.length) {}
|
||||
SanitizableString::SanitizableString(const es_string_token_t &tok) : data_(tok.data, tok.length) {}
|
||||
|
||||
SanitizableString::SanitizableString(NSString *str)
|
||||
: data_([str UTF8String]), length_([str length]) {}
|
||||
SanitizableString::SanitizableString(NSString *str) : data_(NSStringToUTF8StringView(str)) {}
|
||||
|
||||
SanitizableString::SanitizableString(const char *str, size_t len) : data_(str), length_(len) {}
|
||||
SanitizableString::SanitizableString(const char *str, size_t len) : data_(str, len) {}
|
||||
|
||||
std::string_view SanitizableString::String() const {
|
||||
return std::string_view(data_, length_);
|
||||
return data_;
|
||||
}
|
||||
|
||||
std::string_view SanitizableString::Sanitized() const {
|
||||
if (!sanitized_) {
|
||||
sanitized_ = true;
|
||||
sanitized_string_ = SanitizeString(data_, length_);
|
||||
sanitized_string_ = SanitizeString(data_.data(), data_.length());
|
||||
}
|
||||
|
||||
if (sanitized_string_.has_value()) {
|
||||
return sanitized_string_.value();
|
||||
} else {
|
||||
if (data_) {
|
||||
return std::string_view(data_, length_);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
return data_;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,14 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
@class SNTStoredEvent;
|
||||
|
||||
@@ -30,11 +32,11 @@ namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
class Serializer {
|
||||
public:
|
||||
Serializer();
|
||||
Serializer(SNTDecisionCache *decision_cache);
|
||||
virtual ~Serializer() = default;
|
||||
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage> msg) {
|
||||
std::unique_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage> msg) {
|
||||
return std::visit([this](const auto &arg) { return this->SerializeMessageTemplate(arg); },
|
||||
msg->GetEnrichedMessage());
|
||||
}
|
||||
@@ -96,6 +98,7 @@ class Serializer {
|
||||
|
||||
bool enabled_machine_id_ = false;
|
||||
std::string machine_id_;
|
||||
SNTDecisionCache *decision_cache_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace es = santa::santad::event_providers::endpoint_security;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
Serializer::Serializer() {
|
||||
Serializer::Serializer(SNTDecisionCache *decision_cache) : decision_cache_(decision_cache) {
|
||||
if ([[SNTConfigurator configurator] enableMachineIDDecoration]) {
|
||||
enabled_machine_id_ = true;
|
||||
machine_id_ = [[[SNTConfigurator configurator] machineID] UTF8String] ?: "";
|
||||
@@ -46,17 +46,15 @@ std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedExch
|
||||
return SerializeMessage(msg);
|
||||
}
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedExec &msg) {
|
||||
static SNTDecisionCache *decision_cache = [SNTDecisionCache sharedCache];
|
||||
|
||||
SNTCachedDecision *cd;
|
||||
const es_message_t &es_msg = msg.es_msg();
|
||||
if (es_msg.action_type == ES_ACTION_TYPE_NOTIFY &&
|
||||
es_msg.action.notify.result.auth == ES_AUTH_RESULT_ALLOW) {
|
||||
// For allowed execs, cached decision timestamps must be updated
|
||||
cd = [decision_cache
|
||||
cd = [decision_cache_
|
||||
resetTimestampForCachedDecision:msg.es_msg().event.exec.target->executable->stat];
|
||||
} else {
|
||||
cd = [decision_cache cachedDecisionForFile:msg.es_msg().event.exec.target->executable->stat];
|
||||
cd = [decision_cache_ cachedDecisionForFile:msg.es_msg().event.exec.target->executable->stat];
|
||||
}
|
||||
|
||||
return SerializeMessage(msg, cd);
|
||||
|
||||
@@ -56,7 +56,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
|
||||
|
||||
@@ -34,10 +34,29 @@ class FilePeer : public File {
|
||||
using File::ShouldFlush;
|
||||
using File::WatchLogFile;
|
||||
|
||||
NSFileHandle *FileHandle() { return file_handle_; }
|
||||
// Member accesses wrapped in a dispatch_sync to satisfy tsan.
|
||||
NSFileHandle *FileHandle() {
|
||||
__block NSFileHandle *h;
|
||||
dispatch_sync(q_, ^{
|
||||
h = file_handle_;
|
||||
});
|
||||
return h;
|
||||
}
|
||||
|
||||
size_t InternalBufferSize() { return buffer_offset_; }
|
||||
size_t InternalBufferCapacity() { return buffer_.capacity(); }
|
||||
size_t InternalBufferSize() {
|
||||
__block size_t s = 0;
|
||||
dispatch_sync(q_, ^{
|
||||
s = buffer_offset_;
|
||||
});
|
||||
return s;
|
||||
}
|
||||
size_t InternalBufferCapacity() {
|
||||
__block size_t s = 0;
|
||||
dispatch_sync(q_, ^{
|
||||
s = buffer_.capacity();
|
||||
});
|
||||
return s;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
@@ -53,7 +53,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,
|
||||
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *));
|
||||
SNTMetricCounter *rate_limit_counts, SNTMetricSet *metric_set,
|
||||
void (^run_on_first_start)(Metrics *));
|
||||
|
||||
~Metrics();
|
||||
|
||||
@@ -68,6 +69,8 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
void SetEventMetrics(Processor processor, es_event_type_t event_type,
|
||||
EventDisposition disposition, int64_t nanos);
|
||||
|
||||
void SetRateLimitingMetrics(Processor processor, int64_t events_rate_limited_count);
|
||||
|
||||
friend class santa::santad::MetricsPeer;
|
||||
|
||||
private:
|
||||
@@ -80,6 +83,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
uint64_t interval_;
|
||||
SNTMetricInt64Gauge *event_processing_times_;
|
||||
SNTMetricCounter *event_counts_;
|
||||
SNTMetricCounter *rate_limit_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
|
||||
@@ -94,6 +98,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
// Small caches for storing event metrics between metrics export operations
|
||||
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_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad
|
||||
|
||||
@@ -124,9 +124,14 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
|
||||
fieldNames:@[ @"Processor", @"Event", @"Disposition" ]
|
||||
helpText:@"Events received and processed by each processor"];
|
||||
|
||||
SNTMetricCounter *rate_limit_counts =
|
||||
[metric_set counterWithName:@"/santa/rate_limit_count"
|
||||
fieldNames:@[ @"Processor" ]
|
||||
helpText:@"Events rate limited by each processor"];
|
||||
|
||||
std::shared_ptr<Metrics> metrics =
|
||||
std::make_shared<Metrics>(q, timer_source, interval, event_processing_times, event_counts,
|
||||
metric_set, ^(Metrics *metrics) {
|
||||
rate_limit_counts, metric_set, ^(Metrics *metrics) {
|
||||
SNTRegisterCoreMetrics();
|
||||
metrics->EstablishConnection();
|
||||
});
|
||||
@@ -146,12 +151,14 @@ 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,
|
||||
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *))
|
||||
SNTMetricCounter *rate_limit_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),
|
||||
metric_set_(metric_set),
|
||||
run_on_first_start_(run_on_first_start) {
|
||||
SetInterval(interval_);
|
||||
@@ -211,9 +218,16 @@ void Metrics::FlushMetrics() {
|
||||
[event_processing_times_ set:kv.second forFieldValues:@[ processorName, eventName ]];
|
||||
}
|
||||
|
||||
for (const auto &kv : rate_limit_counts_cache_) {
|
||||
NSString *processorName = ProcessorToString(kv.first);
|
||||
|
||||
[rate_limit_counts_ incrementBy:kv.second forFieldValues:@[ processorName ]];
|
||||
}
|
||||
|
||||
// Reset the maps so the next cycle begins with a clean state
|
||||
event_counts_cache_ = {};
|
||||
event_times_cache_ = {};
|
||||
rate_limit_counts_cache_ = {};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -263,4 +277,10 @@ void Metrics::SetEventMetrics(Processor processor, es_event_type_t event_type,
|
||||
});
|
||||
}
|
||||
|
||||
void Metrics::SetRateLimitingMetrics(Processor processor, int64_t events_rate_limited_count) {
|
||||
dispatch_sync(events_q_, ^{
|
||||
rate_limit_counts_cache_[processor] += events_rate_limited_count;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace santa::santad
|
||||
|
||||
@@ -48,6 +48,7 @@ class MetricsPeer : public Metrics {
|
||||
|
||||
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_; };
|
||||
};
|
||||
|
||||
} // namespace santa::santad
|
||||
@@ -72,10 +73,10 @@ 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, ^(santa::santad::Metrics *m) {
|
||||
dispatch_semaphore_signal(self.sema);
|
||||
});
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m) {
|
||||
dispatch_semaphore_signal(self.sema);
|
||||
});
|
||||
|
||||
XCTAssertFalse(metrics->IsRunning());
|
||||
|
||||
@@ -108,7 +109,7 @@ using santa::santad::ProcessorToString;
|
||||
|
||||
- (void)testSetInterval {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil,
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
});
|
||||
|
||||
@@ -182,7 +183,7 @@ using santa::santad::ProcessorToString;
|
||||
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,
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
@@ -221,6 +222,32 @@ using santa::santad::ProcessorToString;
|
||||
XCTAssertEqual(metrics->EventTimes()[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,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
|
||||
// Initial map is empty
|
||||
XCTAssertEqual(metrics->RateLimitCounts().size(), 0);
|
||||
|
||||
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 123);
|
||||
|
||||
// Check sizes after setting metrics once
|
||||
XCTAssertEqual(metrics->RateLimitCounts().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);
|
||||
|
||||
// Check map values
|
||||
XCTAssertEqual(metrics->RateLimitCounts()[Processor::kFileAccessAuthorizer], 123 + 456);
|
||||
XCTAssertEqual(metrics->RateLimitCounts()[Processor::kAuthorizer], 789);
|
||||
}
|
||||
|
||||
- (void)testFlushMetrics {
|
||||
id mockEventProcessingTimes = OCMClassMock([SNTMetricInt64Gauge class]);
|
||||
id mockEventCounts = OCMClassMock([SNTMetricCounter class]);
|
||||
@@ -240,7 +267,7 @@ using santa::santad::ProcessorToString;
|
||||
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, mockEventProcessingTimes,
|
||||
mockEventCounts, nil,
|
||||
mockEventCounts, mockEventCounts, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
@@ -249,23 +276,28 @@ using santa::santad::ProcessorToString;
|
||||
EventDisposition::kProcessed, nanos);
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN,
|
||||
EventDisposition::kProcessed, nanos * 2);
|
||||
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 123);
|
||||
|
||||
// First ensure we have the expected map sizes
|
||||
XCTAssertEqual(metrics->EventCounts().size(), 2);
|
||||
XCTAssertEqual(metrics->EventTimes().size(), 2);
|
||||
XCTAssertEqual(metrics->RateLimitCounts().size(), 1);
|
||||
|
||||
metrics->FlushMetrics();
|
||||
|
||||
// After setting two different event metrics, we expect the sema to be hit
|
||||
// four times - twice each for the event counts and event times maps
|
||||
// five times - twice each for the event counts and event times maps, and
|
||||
// once for the rate limit count map.
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (1)");
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (2)");
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (3)");
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (4)");
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (5)");
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -144,7 +144,10 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
// Check if there is an existing (non-transitive) rule for this file. We leave existing rules
|
||||
// alone, so that a allowlist or blocklist rule can't be overwritten by a transitive one.
|
||||
SNTRuleTable *ruleTable = [SNTDatabaseController ruleTable];
|
||||
SNTRule *prevRule = [ruleTable ruleForBinarySHA256:fi.SHA256 certificateSHA256:nil teamID:nil];
|
||||
SNTRule *prevRule = [ruleTable ruleForBinarySHA256:fi.SHA256
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
if (!prevRule || prevRule.state == SNTRuleStateAllowTransitive) {
|
||||
// Construct a new transitive allowlist rule for the executable.
|
||||
SNTRule *rule = [[SNTRule alloc] initWithIdentifier:fi.SHA256
|
||||
|
||||
@@ -39,6 +39,7 @@ using santa::santad::data_layer::WatchItems;
|
||||
using santa::santad::data_layer::WatchItemsState;
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::FlushCacheReason;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
|
||||
// Globals used by the santad watchdog thread
|
||||
@@ -84,12 +85,9 @@ double watchdogRAMPeak = 0;
|
||||
reply([counts[0] unsignedLongLongValue], [counts[1] unsignedLongLongValue]);
|
||||
}
|
||||
|
||||
- (void)flushAllCaches {
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
}
|
||||
|
||||
- (void)flushCache:(void (^)(BOOL))reply {
|
||||
[self flushAllCaches];
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kAllCaches,
|
||||
FlushCacheReason::kExplicitCommand);
|
||||
reply(YES);
|
||||
}
|
||||
|
||||
@@ -100,10 +98,10 @@ double watchdogRAMPeak = 0;
|
||||
#pragma mark Database ops
|
||||
|
||||
- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive, int64_t teamID))reply {
|
||||
int64_t transitive, int64_t teamID, int64_t signingID))reply {
|
||||
SNTRuleTable *rdb = [SNTDatabaseController ruleTable];
|
||||
reply([rdb binaryRuleCount], [rdb certificateRuleCount], [rdb compilerRuleCount],
|
||||
[rdb transitiveRuleCount], [rdb teamIDRuleCount]);
|
||||
[rdb transitiveRuleCount], [rdb teamIDRuleCount], [rdb signingIDRuleCount]);
|
||||
}
|
||||
|
||||
- (void)databaseRuleAddRules:(NSArray *)rules
|
||||
@@ -125,7 +123,7 @@ double watchdogRAMPeak = 0;
|
||||
// The actual cache flushing happens after the new rules have been added to the database.
|
||||
if (flushCache) {
|
||||
LOGI(@"Flushing caches");
|
||||
[self flushAllCaches];
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kAllCaches, FlushCacheReason::kRulesChanged);
|
||||
}
|
||||
|
||||
reply(error);
|
||||
@@ -146,8 +144,10 @@ double watchdogRAMPeak = 0;
|
||||
- (void)databaseRuleForBinarySHA256:(NSString *)binarySHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
reply:(void (^)(SNTRule *))reply {
|
||||
reply([[SNTDatabaseController ruleTable] ruleForBinarySHA256:binarySHA256
|
||||
signingID:signingID
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID]);
|
||||
}
|
||||
@@ -162,11 +162,13 @@ double watchdogRAMPeak = 0;
|
||||
fileSHA256:(NSString *)fileSHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
reply:(void (^)(SNTEventState))reply {
|
||||
reply([self.policyProcessor decisionForFilePath:filePath
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID]
|
||||
teamID:teamID
|
||||
signingID:signingID]
|
||||
.decision);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user