mirror of
https://github.com/google/santa.git
synced 2026-01-17 02:07:58 -05:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f5f8de245 | ||
|
|
7c58648c35 | ||
|
|
3f3751eb18 | ||
|
|
7aa2d69ce6 | ||
|
|
f9a937a6e4 | ||
|
|
d2cbddd3fb | ||
|
|
ea7e11fc22 | ||
|
|
7530b8f5c1 | ||
|
|
64bb34b2ca | ||
|
|
c5c6037085 | ||
|
|
275a8ed607 | ||
|
|
28dd6cbaed | ||
|
|
8c466b4408 | ||
|
|
373c676306 | ||
|
|
d214d510e5 | ||
|
|
6314fe04e3 | ||
|
|
11d9c29daa | ||
|
|
60238f0ed2 | ||
|
|
7aa731a76f | ||
|
|
5a383ebd9a | ||
|
|
913af692e8 | ||
|
|
4d6140d047 | ||
|
|
2edd2ddfd2 | ||
|
|
1515929752 | ||
|
|
fc2c7ffb71 | ||
|
|
98ee36850a | ||
|
|
6f4a48866c | ||
|
|
51ca19b238 | ||
|
|
b8d7ed0c07 | ||
|
|
ff6bf0701d | ||
|
|
3be45fd6c0 | ||
|
|
d2e5aec635 | ||
|
|
be1169ffcb | ||
|
|
181c3ae573 | ||
|
|
5f0755efbf | ||
|
|
f0165089a4 | ||
|
|
5c98ef6897 | ||
|
|
e2f8ca9569 | ||
|
|
2029e239ca | ||
|
|
cae3578b62 | ||
|
|
16a8c651d5 | ||
|
|
4fdc1e5e41 | ||
|
|
1cdd04f9eb | ||
|
|
4d0af8838f | ||
|
|
0400e29264 | ||
|
|
2c6da7158d | ||
|
|
b0ab761568 | ||
|
|
b02336613a | ||
|
|
bd86145679 | ||
|
|
6dfd5ba084 | ||
|
|
72e292d80e | ||
|
|
6588c2342b |
1
.bazelrc
1
.bazelrc
@@ -9,6 +9,7 @@ build --copt=-Wno-unknown-warning-option
|
||||
build --copt=-Wno-error=deprecated-non-prototype
|
||||
build --per_file_copt=.*\.mm\$@-std=c++17
|
||||
build --cxxopt=-std=c++17
|
||||
build --host_cxxopt=-std=c++17
|
||||
|
||||
build --copt=-DSANTA_OPEN_SOURCE=1
|
||||
build --cxxopt=-DSANTA_OPEN_SOURCE=1
|
||||
|
||||
@@ -1 +1 @@
|
||||
5.3.0
|
||||
6.3.2
|
||||
|
||||
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
@@ -1,5 +1,4 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -11,15 +10,13 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- 'Source/**'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Run linters
|
||||
run: ./Testing/lint.sh
|
||||
|
||||
build_userspace:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -27,10 +24,9 @@ jobs:
|
||||
os: [macos-11, macos-12, macos-13]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Build Userspace
|
||||
run: bazel build --apple_generate_dsym -c opt :release --define=SANTA_BUILD_TYPE=adhoc
|
||||
|
||||
unit_tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -38,18 +34,17 @@ jobs:
|
||||
os: [macos-11, macos-12, macos-13]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Run All Tests
|
||||
run: bazel test :unit_tests --define=SANTA_BUILD_TYPE=adhoc --test_output=errors
|
||||
|
||||
test_coverage:
|
||||
runs-on: macos-11
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Generate test coverage
|
||||
run: sh ./generate_cov.sh
|
||||
- name: Coveralls
|
||||
uses: coverallsapp/github-action@master
|
||||
uses: coverallsapp/github-action@09b709cf6a16e30b0808ba050c7a6e8a5ef13f8d # ratchet:coverallsapp/github-action@master
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path-to-lcov: ./bazel-out/_coverage/_coverage_report.dat
|
||||
|
||||
17
.github/workflows/e2e.yml
vendored
17
.github/workflows/e2e.yml
vendored
@@ -1,25 +1,28 @@
|
||||
name: E2E
|
||||
|
||||
on: workflow_dispatch
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 4 * * *' # Every day at 4:00 UTC (not to interfere with fuzzing)
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
start_vm:
|
||||
runs-on: e2e-host
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Start VM
|
||||
run: python3 Testing/integration/actions/start_vm.py macOS_12.bundle.tar.gz
|
||||
run: python3 Testing/integration/actions/start_vm.py macOS_14.bundle.tar.gz
|
||||
|
||||
integration:
|
||||
runs-on: e2e-vm
|
||||
env:
|
||||
VM_PASSWORD: ${{ secrets.VM_PASSWORD }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install configuration profile
|
||||
run: bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
|
||||
- name: Add homebrew to PATH
|
||||
run: echo "/opt/homebrew/bin/" >> $GITHUB_PATH
|
||||
- name: Install configuration profile
|
||||
run: bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig
|
||||
- name: Build, install, and start moroz
|
||||
run: |
|
||||
bazel build @com_github_groob_moroz//cmd/moroz:moroz
|
||||
@@ -36,6 +39,8 @@ jobs:
|
||||
run: ./Testing/integration/test_config_changes.sh
|
||||
- name: Test sync server changes
|
||||
run: ./Testing/integration/test_sync_changes.sh
|
||||
- name: Test USB blocking
|
||||
run: ./Testing/integration/test_usb.sh
|
||||
- name: Poweroff
|
||||
if: ${{ always() }}
|
||||
run: sudo shutdown -h +1
|
||||
|
||||
10
README.md
10
README.md
@@ -7,10 +7,10 @@
|
||||
[](https://github.com/google/santa/releases/latest)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/google/santa/main/Source/gui/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
|
||||
<img src="./docs/images/santa-sleigh-256.png" height="128" alt="Santa Icon" />
|
||||
</p>
|
||||
|
||||
Santa is a binary authorization system for macOS. It consists of a system
|
||||
Santa is a binary and file access authorization system for macOS. It consists of a system
|
||||
extension that monitors for executions, a daemon that makes execution decisions
|
||||
based on the contents of a local database, a GUI agent that notifies the user in
|
||||
case of a block decision and a command-line utility for managing the system and
|
||||
@@ -48,9 +48,7 @@ disclosure reporting.
|
||||
the events database. In LOCKDOWN mode, only listed binaries are allowed to
|
||||
run.
|
||||
|
||||
* Event logging: When the kext is loaded, all binary launches are logged. When
|
||||
in either mode, all unknown or denied binaries are stored in the database to
|
||||
enable later aggregation.
|
||||
* Event logging: When the system extension is loaded, all binary launches are logged. When in either mode, all unknown or denied binaries are stored in the database to enable later aggregation.
|
||||
|
||||
* Certificate-based rules, with override levels: Instead of relying on a
|
||||
binary's hash (or 'fingerprint'), executables can be allowed/blocked by their
|
||||
@@ -140,7 +138,7 @@ A tool like Santa doesn't really lend itself to screenshots, so here's a video
|
||||
instead.
|
||||
|
||||
|
||||
<p align="center"> <img src="https://thumbs.gfycat.com/MadFatalAmphiuma-small.gif" alt="Santa Block Video" /> </p>
|
||||
<p align="center"> <img src="./docs/images/santa-block.gif" alt="Santa Block Video" /> </p>
|
||||
|
||||
# Contributing
|
||||
Patches to this project are very much welcome. Please see the
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//:helper.bzl", "santa_unit_test")
|
||||
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
|
||||
load("//:helper.bzl", "santa_unit_test")
|
||||
|
||||
package(
|
||||
default_visibility = ["//:santa_package_group"],
|
||||
@@ -84,12 +84,22 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "CertificateHelpers",
|
||||
srcs = ["CertificateHelpers.m"],
|
||||
hdrs = ["CertificateHelpers.h"],
|
||||
deps = [
|
||||
"@MOLCertificate",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTBlockMessage",
|
||||
srcs = ["SNTBlockMessage.m"],
|
||||
hdrs = ["SNTBlockMessage.h"],
|
||||
deps = [
|
||||
":SNTConfigurator",
|
||||
":SNTFileAccessEvent",
|
||||
":SNTLogging",
|
||||
":SNTStoredEvent",
|
||||
":SNTSystemInfo",
|
||||
@@ -103,7 +113,7 @@ objc_library(
|
||||
defines = ["SANTAGUI"],
|
||||
deps = [
|
||||
":SNTConfigurator",
|
||||
":SNTDeviceEvent",
|
||||
":SNTFileAccessEvent",
|
||||
":SNTLogging",
|
||||
":SNTStoredEvent",
|
||||
":SNTSystemInfo",
|
||||
@@ -142,6 +152,7 @@ objc_library(
|
||||
"Foundation",
|
||||
],
|
||||
deps = [
|
||||
":CertificateHelpers",
|
||||
"@MOLCertificate",
|
||||
],
|
||||
)
|
||||
@@ -241,6 +252,7 @@ santa_unit_test(
|
||||
deps = [
|
||||
":SNTCommonEnums",
|
||||
":SNTRule",
|
||||
":SNTSyncConstants",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -396,10 +408,24 @@ santa_unit_test(
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTBlockMessageTest",
|
||||
srcs = ["SNTBlockMessageTest.m"],
|
||||
deps = [
|
||||
":SNTBlockMessage",
|
||||
":SNTConfigurator",
|
||||
":SNTFileAccessEvent",
|
||||
":SNTStoredEvent",
|
||||
":SNTSystemInfo",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "unit_tests",
|
||||
tests = [
|
||||
":PrefixTreeTest",
|
||||
":SNTBlockMessageTest",
|
||||
":SNTCachedDecisionTest",
|
||||
":SNTFileInfoTest",
|
||||
":SNTKVOManagerTest",
|
||||
|
||||
43
Source/common/CertificateHelpers.h
Normal file
43
Source/common/CertificateHelpers.h
Normal file
@@ -0,0 +1,43 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#include <sys/cdefs.h>
|
||||
|
||||
__BEGIN_DECLS
|
||||
|
||||
/**
|
||||
Return a string representing publisher info from the provided certs
|
||||
|
||||
@param certs A certificate chain
|
||||
@param teamID A team ID to be displayed for apps from the App Store
|
||||
|
||||
@return A string that tries to be more helpful to users by extracting
|
||||
appropriate information from the certificate chain.
|
||||
*/
|
||||
NSString *Publisher(NSArray<MOLCertificate *> *certs, NSString *teamID);
|
||||
|
||||
/**
|
||||
Return an array of the underlying SecCertificateRef's for the given array
|
||||
of MOLCertificates.
|
||||
|
||||
@param certs An array of MOLCertificates
|
||||
|
||||
@return An array of SecCertificateRefs. WARNING: If the refs need to be used
|
||||
for a long time be careful to properly CFRetain/CFRelease the returned items.
|
||||
*/
|
||||
NSArray<id> *CertificateChain(NSArray<MOLCertificate *> *certs);
|
||||
|
||||
__END_DECLS
|
||||
42
Source/common/CertificateHelpers.m
Normal file
42
Source/common/CertificateHelpers.m
Normal file
@@ -0,0 +1,42 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/CertificateHelpers.h"
|
||||
|
||||
#include <Security/SecCertificate.h>
|
||||
|
||||
NSString *Publisher(NSArray<MOLCertificate *> *certs, NSString *teamID) {
|
||||
MOLCertificate *leafCert = [certs firstObject];
|
||||
|
||||
if ([leafCert.commonName isEqualToString:@"Apple Mac OS Application Signing"]) {
|
||||
return [NSString stringWithFormat:@"App Store (Team ID: %@)", teamID];
|
||||
} else if (leafCert.commonName && leafCert.orgName) {
|
||||
return [NSString stringWithFormat:@"%@ - %@", leafCert.orgName, leafCert.commonName];
|
||||
} else if (leafCert.commonName) {
|
||||
return leafCert.commonName;
|
||||
} else if (leafCert.orgName) {
|
||||
return leafCert.orgName;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSArray<id> *CertificateChain(NSArray<MOLCertificate *> *certs) {
|
||||
NSMutableArray *certArray = [NSMutableArray arrayWithCapacity:[certs count]];
|
||||
for (MOLCertificate *cert in certs) {
|
||||
[certArray addObject:(id)cert.certRef];
|
||||
}
|
||||
|
||||
return certArray;
|
||||
}
|
||||
@@ -18,6 +18,9 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#endif
|
||||
|
||||
#import "Source/common/SNTFileAccessEvent.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
|
||||
@class SNTStoredEvent;
|
||||
|
||||
@interface SNTBlockMessage : NSObject
|
||||
@@ -38,11 +41,15 @@
|
||||
+ (NSAttributedString *)attributedBlockMessageForEvent:(SNTStoredEvent *)event
|
||||
customMessage:(NSString *)customMessage;
|
||||
|
||||
+ (NSAttributedString *)attributedBlockMessageForFileAccessEvent:(SNTFileAccessEvent *)event
|
||||
customMessage:(NSString *)customMessage;
|
||||
|
||||
///
|
||||
/// Return a URL generated from the EventDetailURL configuration key
|
||||
/// after replacing templates in the URL with values from the event.
|
||||
///
|
||||
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event customURL:(NSString *)url;
|
||||
+ (NSURL *)eventDetailURLForFileAccessEvent:(SNTFileAccessEvent *)event customURL:(NSString *)url;
|
||||
|
||||
///
|
||||
/// Strip HTML from a string, replacing <br /> with newline.
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTFileAccessEvent.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/common/SNTSystemInfo.h"
|
||||
@@ -82,6 +83,18 @@
|
||||
return [SNTBlockMessage formatMessage:message];
|
||||
}
|
||||
|
||||
+ (NSAttributedString *)attributedBlockMessageForFileAccessEvent:(SNTFileAccessEvent *)event
|
||||
customMessage:(NSString *)customMessage {
|
||||
NSString *message = customMessage;
|
||||
if (!message.length) {
|
||||
message = [[SNTConfigurator configurator] fileAccessBlockMessage];
|
||||
if (!message.length) {
|
||||
message = @"Access to a file has been denied.";
|
||||
}
|
||||
}
|
||||
return [SNTBlockMessage formatMessage:message];
|
||||
}
|
||||
|
||||
+ (NSString *)stringFromHTML:(NSString *)html {
|
||||
NSError *error;
|
||||
NSXMLDocument *xml = [[NSXMLDocument alloc] initWithXMLString:html options:0 error:&error];
|
||||
@@ -109,6 +122,21 @@
|
||||
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
+ (NSString *)replaceFormatString:(NSString *)str
|
||||
withDict:(NSDictionary<NSString *, NSString * (^)()> *)replacements {
|
||||
__block NSString *formatStr = str;
|
||||
|
||||
[replacements
|
||||
enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString * (^computeValue)(), BOOL *stop) {
|
||||
NSString *value = computeValue();
|
||||
if (value) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:key withString:value];
|
||||
}
|
||||
}];
|
||||
|
||||
return formatStr;
|
||||
}
|
||||
|
||||
// Returns either the generated URL for the passed in event, or an NSURL from the passed in custom
|
||||
// URL string. If the custom URL string is the string "null", nil will be returned. If no custom
|
||||
// URL is passed and there is no configured EventDetailURL template, nil will be returned.
|
||||
@@ -126,47 +154,94 @@
|
||||
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event customURL:(NSString *)url {
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
|
||||
NSString *hostname = [SNTSystemInfo longHostname];
|
||||
NSString *uuid = [SNTSystemInfo hardwareUUID];
|
||||
NSString *serial = [SNTSystemInfo serialNumber];
|
||||
|
||||
NSString *formatStr = url;
|
||||
if (!url.length) formatStr = config.eventDetailURL;
|
||||
if (!formatStr.length) return nil;
|
||||
if ([formatStr isEqualToString:@"null"]) return nil;
|
||||
if (!formatStr.length) {
|
||||
formatStr = config.eventDetailURL;
|
||||
if (!formatStr.length) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.fileSHA256) {
|
||||
// This key is deprecated, use %file_identifier% or %bundle_or_file_identifier%
|
||||
formatStr =
|
||||
[formatStr stringByReplacingOccurrencesOfString:@"%file_sha%"
|
||||
withString:event.fileBundleHash ?: event.fileSHA256];
|
||||
if ([formatStr isEqualToString:@"null"]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%file_identifier%"
|
||||
withString:event.fileSHA256];
|
||||
formatStr =
|
||||
[formatStr stringByReplacingOccurrencesOfString:@"%bundle_or_file_identifier%"
|
||||
withString:event.fileBundleHash ?: event.fileSHA256];
|
||||
}
|
||||
if (event.executingUser) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%username%"
|
||||
withString:event.executingUser];
|
||||
}
|
||||
if (config.machineID) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%machine_id%"
|
||||
withString:config.machineID];
|
||||
}
|
||||
if (hostname.length) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%hostname%" withString:hostname];
|
||||
}
|
||||
if (uuid.length) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%uuid%" withString:uuid];
|
||||
}
|
||||
if (serial.length) {
|
||||
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%serial%" withString:serial];
|
||||
}
|
||||
// Disabling clang-format. See comment in `eventDetailURLForFileAccessEvent:customURL:`
|
||||
// clang-format off
|
||||
NSDictionary<NSString *, NSString * (^)()> *kvReplacements =
|
||||
[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
// This key is deprecated, use %file_identifier% or %bundle_or_file_identifier%
|
||||
^{ return event.fileSHA256 ? event.fileBundleHash ?: event.fileSHA256 : nil; },
|
||||
@"%file_sha%",
|
||||
^{ return event.fileSHA256; }, @"%file_identifier%",
|
||||
^{ return event.fileSHA256 ? event.fileBundleHash ?: event.fileSHA256 : nil; },
|
||||
@"%bundle_or_file_identifier%",
|
||||
^{ return event.executingUser; }, @"%username%",
|
||||
^{ return config.machineID; }, @"%machine_id%",
|
||||
^{ return [SNTSystemInfo longHostname]; }, @"%hostname%",
|
||||
^{ return [SNTSystemInfo hardwareUUID]; }, @"%uuid%",
|
||||
^{ return [SNTSystemInfo serialNumber]; }, @"%serial%",
|
||||
nil];
|
||||
// clang-format on
|
||||
|
||||
formatStr = [SNTBlockMessage replaceFormatString:formatStr withDict:kvReplacements];
|
||||
|
||||
NSURL *u = [NSURL URLWithString:formatStr];
|
||||
if (!u) LOGW(@"Unable to generate event detail URL for string '%@'", formatStr);
|
||||
if (!u) {
|
||||
LOGW(@"Unable to generate event detail URL for string '%@'", formatStr);
|
||||
}
|
||||
|
||||
return u;
|
||||
}
|
||||
|
||||
// Returns either the generated URL for the passed in event, or an NSURL from the passed in custom
|
||||
// URL string. If the custom URL string is the string "null", nil will be returned. If no custom
|
||||
// URL is passed and there is no configured EventDetailURL template, nil will be returned.
|
||||
// The following "format strings" will be replaced in the URL, if they are present:
|
||||
//
|
||||
// %rule_version% - The version of the rule that was violated.
|
||||
// %rule_name% - The name of the rule that was violated.
|
||||
// %file_identifier% - The SHA-256 of the binary being executed.
|
||||
// %accessed_path% - The path accessed by the binary.
|
||||
// %username% - The executing user's name.
|
||||
// %machine_id% - The configured machine ID for this host.
|
||||
// %hostname% - The machine's FQDN.
|
||||
// %uuid% - The machine's UUID.
|
||||
// %serial% - The machine's serial number.
|
||||
//
|
||||
+ (NSURL *)eventDetailURLForFileAccessEvent:(SNTFileAccessEvent *)event customURL:(NSString *)url {
|
||||
if (!url.length || [url isEqualToString:@"null"]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
|
||||
// Clang format goes wild here. If you use the container literal syntax `@{}` with a block value
|
||||
// type, it seems to break the clang format on/off functionality and breaks formatting for the
|
||||
// remainder of the file.
|
||||
// Using `dictionaryWithObjectsAndKeys` and disabling clang format as a workaround.
|
||||
// clang-format off
|
||||
NSDictionary<NSString *, NSString * (^)()> *kvReplacements =
|
||||
[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
^{ return event.ruleVersion; }, @"%rule_version%",
|
||||
^{ return event.ruleName; }, @"%rule_name%",
|
||||
^{ return event.fileSHA256; }, @"%file_identifier%",
|
||||
^{ return event.accessedPath; }, @"%accessed_path%",
|
||||
^{ return event.executingUser; }, @"%username%",
|
||||
^{ return config.machineID; }, @"%machine_id%",
|
||||
^{ return [SNTSystemInfo longHostname]; }, @"%hostname%",
|
||||
^{ return [SNTSystemInfo hardwareUUID]; }, @"%uuid%",
|
||||
^{ return [SNTSystemInfo serialNumber]; }, @"%serial%",
|
||||
nil];
|
||||
// clang-format on
|
||||
|
||||
NSString *formatStr = [SNTBlockMessage replaceFormatString:url withDict:kvReplacements];
|
||||
|
||||
NSURL *u = [NSURL URLWithString:formatStr];
|
||||
if (!u) {
|
||||
LOGW(@"Unable to generate event detail URL for string '%@'", formatStr);
|
||||
}
|
||||
|
||||
return u;
|
||||
}
|
||||
|
||||
|
||||
95
Source/common/SNTBlockMessageTest.m
Normal file
95
Source/common/SNTBlockMessageTest.m
Normal file
@@ -0,0 +1,95 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#include "Source/common/SNTFileAccessEvent.h"
|
||||
#include "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/common/SNTSystemInfo.h"
|
||||
|
||||
@interface SNTBlockMessageTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@property id mockSystemInfo;
|
||||
@end
|
||||
|
||||
@implementation SNTBlockMessageTest
|
||||
|
||||
- (void)setUp {
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator machineID]).andReturn(@"my_mid");
|
||||
|
||||
self.mockSystemInfo = OCMClassMock([SNTSystemInfo class]);
|
||||
OCMStub([self.mockSystemInfo longHostname]).andReturn(@"my_hn");
|
||||
OCMStub([self.mockSystemInfo hardwareUUID]).andReturn(@"my_u");
|
||||
OCMStub([self.mockSystemInfo serialNumber]).andReturn(@"my_s");
|
||||
}
|
||||
|
||||
- (void)testEventDetailURLForEvent {
|
||||
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
|
||||
|
||||
se.fileSHA256 = @"my_fi";
|
||||
se.executingUser = @"my_un";
|
||||
|
||||
NSString *url = @"http://"
|
||||
@"localhost?fs=%file_sha%&fi=%file_identifier%&bfi=%bundle_or_file_identifier%&"
|
||||
@"un=%username%&mid=%machine_id%&hn=%hostname%&u=%uuid%&s=%serial%";
|
||||
NSString *wantUrl =
|
||||
@"http://"
|
||||
@"localhost?fs=my_fi&fi=my_fi&bfi=my_fi&bfi=my_fi&un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
|
||||
|
||||
NSURL *gotUrl = [SNTBlockMessage eventDetailURLForEvent:se customURL:url];
|
||||
|
||||
// Set fileBundleHash and test again for newly expected values
|
||||
se.fileBundleHash = @"my_fbh";
|
||||
|
||||
wantUrl = @"http://"
|
||||
@"localhost?fs=my_fbh&fi=my_fi&bfi=my_fbh&un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
|
||||
|
||||
gotUrl = [SNTBlockMessage eventDetailURLForEvent:se customURL:url];
|
||||
|
||||
XCTAssertEqualObjects(gotUrl.absoluteString, wantUrl);
|
||||
|
||||
XCTAssertNil([SNTBlockMessage eventDetailURLForEvent:se customURL:nil]);
|
||||
XCTAssertNil([SNTBlockMessage eventDetailURLForEvent:se customURL:@"null"]);
|
||||
}
|
||||
|
||||
- (void)testEventDetailURLForFileAccessEvent {
|
||||
SNTFileAccessEvent *fae = [[SNTFileAccessEvent alloc] init];
|
||||
|
||||
fae.ruleVersion = @"my_rv";
|
||||
fae.ruleName = @"my_rn";
|
||||
fae.fileSHA256 = @"my_fi";
|
||||
fae.accessedPath = @"my_ap";
|
||||
fae.executingUser = @"my_un";
|
||||
|
||||
NSString *url = @"http://"
|
||||
@"localhost?rv=%rule_version%&rn=%rule_name%&fi=%file_identifier%&ap=%accessed_"
|
||||
@"path%&un=%username%&mid=%machine_id%&hn=%hostname%&u=%uuid%&s=%serial%";
|
||||
NSString *wantUrl =
|
||||
@"http://"
|
||||
@"localhost?rv=my_rv&rn=my_rn&fi=my_fi&ap=my_ap&un=my_un&mid=my_mid&hn=my_hn&u=my_u&s=my_s";
|
||||
|
||||
NSURL *gotUrl = [SNTBlockMessage eventDetailURLForFileAccessEvent:fae customURL:url];
|
||||
|
||||
XCTAssertEqualObjects(gotUrl.absoluteString, wantUrl);
|
||||
|
||||
XCTAssertNil([SNTBlockMessage eventDetailURLForFileAccessEvent:fae customURL:nil]);
|
||||
XCTAssertNil([SNTBlockMessage eventDetailURLForFileAccessEvent:fae customURL:@"null"]);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -152,6 +152,20 @@ typedef NS_ENUM(NSInteger, SNTMetricFormatType) {
|
||||
SNTMetricFormatTypeMonarchJSON,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTOverrideFileAccessAction) {
|
||||
SNTOverrideFileAccessActionNone,
|
||||
SNTOverrideFileAccessActionAuditOnly,
|
||||
SNTOverrideFileAccessActionDiable,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTDeviceManagerStartupPreferences) {
|
||||
SNTDeviceManagerStartupPreferencesNone,
|
||||
SNTDeviceManagerStartupPreferencesUnmount,
|
||||
SNTDeviceManagerStartupPreferencesForceUnmount,
|
||||
SNTDeviceManagerStartupPreferencesRemount,
|
||||
SNTDeviceManagerStartupPreferencesForceRemount,
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
enum class FileAccessPolicyDecision {
|
||||
kNoPolicy,
|
||||
|
||||
@@ -262,6 +262,16 @@
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *fileAccessPolicyPlist;
|
||||
|
||||
///
|
||||
/// This is the message shown to the user when access to a file is blocked
|
||||
/// by a binary due to some rule in the current File Access policy if that rule
|
||||
/// doesn't provide a custom message. If this is not configured, a reasonable
|
||||
/// default is provided.
|
||||
///
|
||||
/// @note: This property is KVO compliant.
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *fileAccessBlockMessage;
|
||||
|
||||
///
|
||||
/// If fileAccessPolicyPlist is set, fileAccessPolicyUpdateIntervalSec
|
||||
/// sets the number of seconds between times that the configuration file is
|
||||
@@ -444,6 +454,39 @@
|
||||
///
|
||||
@property(nonatomic) NSArray<NSString *> *remountUSBMode;
|
||||
|
||||
///
|
||||
/// If set, defines the action that should be taken on existing USB mounts when
|
||||
/// Santa starts up.
|
||||
///
|
||||
/// Supported values are:
|
||||
/// * "Unmount": Unmount mass storage devices
|
||||
/// * "ForceUnmount": Force unmount mass storage devices
|
||||
///
|
||||
///
|
||||
/// Note: Existing mounts with mount flags that are a superset of RemountUSBMode
|
||||
/// are unaffected and left mounted.
|
||||
///
|
||||
@property(readonly, nonatomic) SNTDeviceManagerStartupPreferences onStartUSBOptions;
|
||||
|
||||
///
|
||||
/// If set, will override the action taken when a file access rule violation
|
||||
/// occurs. This setting will apply across all rules in the file access policy.
|
||||
///
|
||||
/// Possible values are
|
||||
/// * "AuditOnly": When a rule is violated, it will be logged, but the access
|
||||
/// will not be blocked
|
||||
/// * "Disable": No access will be logged or blocked.
|
||||
///
|
||||
/// If not set, no override will take place and the file acces spolicy will
|
||||
/// apply as configured.
|
||||
///
|
||||
@property(readonly, nonatomic) SNTOverrideFileAccessAction overrideFileAccessAction;
|
||||
|
||||
///
|
||||
/// Set the action that will override file access policy config action
|
||||
///
|
||||
- (void)setSyncServerOverrideFileAccessAction:(NSString *)action;
|
||||
|
||||
///
|
||||
/// If set, this over-rides the default machine ID used for syncing.
|
||||
///
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
@@ -102,6 +101,7 @@ static NSString *const kSpoolDirectoryEventMaxFlushTimeSec = @"SpoolDirectoryEve
|
||||
|
||||
static NSString *const kFileAccessPolicy = @"FileAccessPolicy";
|
||||
static NSString *const kFileAccessPolicyPlist = @"FileAccessPolicyPlist";
|
||||
static NSString *const kFileAccessBlockMessage = @"FileAccessBlockMessage";
|
||||
static NSString *const kFileAccessPolicyUpdateIntervalSec = @"FileAccessPolicyUpdateIntervalSec";
|
||||
|
||||
static NSString *const kEnableMachineIDDecoration = @"EnableMachineIDDecoration";
|
||||
@@ -121,6 +121,7 @@ static NSString *const kClientModeKey = @"ClientMode";
|
||||
static NSString *const kFailClosedKey = @"FailClosed";
|
||||
static NSString *const kBlockUSBMountKey = @"BlockUSBMount";
|
||||
static NSString *const kRemountUSBModeKey = @"RemountUSBMode";
|
||||
static NSString *const kOnStartUSBOptions = @"OnStartUSBOptions";
|
||||
static NSString *const kEnableTransitiveRulesKey = @"EnableTransitiveRules";
|
||||
static NSString *const kEnableTransitiveRulesKeyDeprecated = @"EnableTransitiveWhitelisting";
|
||||
static NSString *const kAllowedPathRegexKey = @"AllowedPathRegex";
|
||||
@@ -129,6 +130,7 @@ static NSString *const kBlockedPathRegexKey = @"BlockedPathRegex";
|
||||
static NSString *const kBlockedPathRegexKeyDeprecated = @"BlacklistRegex";
|
||||
static NSString *const kEnableAllEventUploadKey = @"EnableAllEventUpload";
|
||||
static NSString *const kDisableUnknownEventUploadKey = @"DisableUnknownEventUpload";
|
||||
static NSString *const kOverrideFileAccessActionKey = @"OverrideFileAccessAction";
|
||||
|
||||
static NSString *const kMetricFormat = @"MetricFormat";
|
||||
static NSString *const kMetricURL = @"MetricURL";
|
||||
@@ -165,6 +167,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kRuleSyncLastSuccess : date,
|
||||
kSyncCleanRequired : number,
|
||||
kEnableAllEventUploadKey : number,
|
||||
kOverrideFileAccessActionKey : string,
|
||||
};
|
||||
_forcedConfigKeyTypes = @{
|
||||
kClientModeKey : number,
|
||||
@@ -179,6 +182,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kBlockedPathRegexKeyDeprecated : re,
|
||||
kBlockUSBMountKey : number,
|
||||
kRemountUSBModeKey : array,
|
||||
kOnStartUSBOptions : string,
|
||||
kEnablePageZeroProtectionKey : number,
|
||||
kEnableBadSignatureProtectionKey : number,
|
||||
kEnableSilentModeKey : number,
|
||||
@@ -219,6 +223,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kSpoolDirectoryEventMaxFlushTimeSec : number,
|
||||
kFileAccessPolicy : dictionary,
|
||||
kFileAccessPolicyPlist : string,
|
||||
kFileAccessBlockMessage : string,
|
||||
kFileAccessPolicyUpdateIntervalSec : number,
|
||||
kEnableMachineIDDecoration : number,
|
||||
kEnableForkAndExitLogging : number,
|
||||
@@ -234,6 +239,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kMetricExtraLabels : dictionary,
|
||||
kEnableAllEventUploadKey : number,
|
||||
kDisableUnknownEventUploadKey : number,
|
||||
kOverrideFileAccessActionKey : string,
|
||||
};
|
||||
_defaults = [NSUserDefaults standardUserDefaults];
|
||||
[_defaults addSuiteNamed:@"com.google.santa"];
|
||||
@@ -441,6 +447,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFileAccessBlockMessage {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicyUpdateIntervalSec {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -513,6 +523,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingOverrideFileAccessActionKey {
|
||||
return [self syncAndConfigStateSet];
|
||||
}
|
||||
|
||||
#pragma mark Public Interface
|
||||
|
||||
- (SNTClientMode)clientMode {
|
||||
@@ -623,6 +637,22 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return args;
|
||||
}
|
||||
|
||||
- (SNTDeviceManagerStartupPreferences)onStartUSBOptions {
|
||||
NSString *action = [self.configState[kOnStartUSBOptions] lowercaseString];
|
||||
|
||||
if ([action isEqualToString:@"unmount"]) {
|
||||
return SNTDeviceManagerStartupPreferencesUnmount;
|
||||
} else if ([action isEqualToString:@"forceunmount"]) {
|
||||
return SNTDeviceManagerStartupPreferencesForceUnmount;
|
||||
} else if ([action isEqualToString:@"remount"]) {
|
||||
return SNTDeviceManagerStartupPreferencesRemount;
|
||||
} else if ([action isEqualToString:@"forceremount"]) {
|
||||
return SNTDeviceManagerStartupPreferencesForceRemount;
|
||||
} else {
|
||||
return SNTDeviceManagerStartupPreferencesNone;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *, SNTRule *> *)staticRules {
|
||||
return self.cachedStaticRules;
|
||||
}
|
||||
@@ -861,6 +891,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)fileAccessBlockMessage {
|
||||
return self.configState[kFileAccessBlockMessage];
|
||||
}
|
||||
|
||||
- (uint32_t)fileAccessPolicyUpdateIntervalSec {
|
||||
return self.configState[kFileAccessPolicyUpdateIntervalSec]
|
||||
? [self.configState[kFileAccessPolicyUpdateIntervalSec] unsignedIntValue]
|
||||
@@ -941,6 +975,33 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self.configState[kBlockUSBMountKey] boolValue];
|
||||
}
|
||||
|
||||
- (void)setSyncServerOverrideFileAccessAction:(NSString *)action {
|
||||
NSString *a = [action lowercaseString];
|
||||
if ([a isEqualToString:@"auditonly"] || [a isEqualToString:@"disable"] ||
|
||||
[a isEqualToString:@"none"] || [a isEqualToString:@""]) {
|
||||
[self updateSyncStateForKey:kOverrideFileAccessActionKey value:action];
|
||||
}
|
||||
}
|
||||
|
||||
- (SNTOverrideFileAccessAction)overrideFileAccessAction {
|
||||
NSString *action = [self.syncState[kOverrideFileAccessActionKey] lowercaseString];
|
||||
|
||||
if (!action) {
|
||||
action = [self.configState[kOverrideFileAccessActionKey] lowercaseString];
|
||||
if (!action) {
|
||||
return SNTOverrideFileAccessActionNone;
|
||||
}
|
||||
}
|
||||
|
||||
if ([action isEqualToString:@"auditonly"]) {
|
||||
return SNTOverrideFileAccessActionAuditOnly;
|
||||
} else if ([action isEqualToString:@"disable"]) {
|
||||
return SNTOverrideFileAccessActionDiable;
|
||||
} else {
|
||||
return SNTOverrideFileAccessActionNone;
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Returns YES if all of the necessary options are set to export metrics, NO
|
||||
/// otherwise.
|
||||
@@ -1073,7 +1134,11 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
NSDictionary *overrides = [NSDictionary dictionaryWithContentsOfFile:kConfigOverrideFilePath];
|
||||
for (NSString *key in overrides) {
|
||||
id obj = overrides[key];
|
||||
if (![obj isKindOfClass:self.forcedConfigKeyTypes[key]]) continue;
|
||||
if (![obj isKindOfClass:self.forcedConfigKeyTypes[key]] ||
|
||||
([self.forcedConfigKeyTypes[key] isKindOfClass:[NSRegularExpression class]] &&
|
||||
![obj isKindOfClass:[NSString class]])) {
|
||||
continue;
|
||||
}
|
||||
forcedConfig[key] = obj;
|
||||
if (self.forcedConfigKeyTypes[key] == [NSRegularExpression class]) {
|
||||
NSString *pattern = [obj isKindOfClass:[NSString class]] ? obj : nil;
|
||||
|
||||
@@ -77,7 +77,23 @@
|
||||
///
|
||||
@property NSString *parentName;
|
||||
|
||||
// TODO(mlw): Store signing chain info
|
||||
// @property NSArray<MOLCertificate*> *signingChain;
|
||||
///
|
||||
/// If the executed file was signed, this is an NSArray of MOLCertificate's
|
||||
/// representing the signing chain.
|
||||
///
|
||||
@property NSArray<MOLCertificate *> *signingChain;
|
||||
|
||||
///
|
||||
/// A string representing the publisher based on the signingChain
|
||||
///
|
||||
@property(readonly) NSString *publisherInfo;
|
||||
|
||||
///
|
||||
/// Return an array of the underlying SecCertificateRef's of the signingChain
|
||||
///
|
||||
/// WARNING: If the refs need to be used for a long time be careful to properly
|
||||
/// CFRetain/CFRelease the returned items.
|
||||
///
|
||||
@property(readonly) NSArray *signingChainCertRefs;
|
||||
|
||||
@end
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
#import "Source/common/SNTFileAccessEvent.h"
|
||||
|
||||
#import "Source/common/CertificateHelpers.h"
|
||||
|
||||
@implementation SNTFileAccessEvent
|
||||
|
||||
#define ENCODE(o) \
|
||||
@@ -28,6 +30,12 @@
|
||||
_##o = [decoder decodeObjectOfClass:[c class] forKey:@(#o)]; \
|
||||
} while (0)
|
||||
|
||||
#define DECODEARRAY(o, c) \
|
||||
do { \
|
||||
_##o = [decoder decodeObjectOfClasses:[NSSet setWithObjects:[NSArray class], [c class], nil] \
|
||||
forKey:@(#o)]; \
|
||||
} while (0)
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
@@ -51,6 +59,7 @@
|
||||
ENCODE(pid);
|
||||
ENCODE(ppid);
|
||||
ENCODE(parentName);
|
||||
ENCODE(signingChain);
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)decoder {
|
||||
@@ -67,6 +76,7 @@
|
||||
DECODE(pid, NSNumber);
|
||||
DECODE(ppid, NSNumber);
|
||||
DECODE(parentName, NSString);
|
||||
DECODEARRAY(signingChain, MOLCertificate);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -76,4 +86,12 @@
|
||||
stringWithFormat:@"SNTFileAccessEvent: Accessed: %@, By: %@", self.accessedPath, self.filePath];
|
||||
}
|
||||
|
||||
- (NSString *)publisherInfo {
|
||||
return Publisher(self.signingChain, self.teamID);
|
||||
}
|
||||
|
||||
- (NSArray *)signingChainCertRefs {
|
||||
return CertificateChain(self.signingChain);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -79,4 +79,9 @@
|
||||
///
|
||||
- (void)resetTimestamp;
|
||||
|
||||
///
|
||||
/// Returns a dictionary representation of the rule.
|
||||
///
|
||||
- (NSDictionary *)dictionaryRepresentation;
|
||||
|
||||
@end
|
||||
|
||||
@@ -226,6 +226,43 @@ static const NSUInteger kExpectedTeamIDLength = 10;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)ruleStateToPolicyString:(SNTRuleState)state {
|
||||
switch (state) {
|
||||
case SNTRuleStateAllow: return kRulePolicyAllowlist;
|
||||
case SNTRuleStateAllowCompiler: return kRulePolicyAllowlistCompiler;
|
||||
case SNTRuleStateBlock: return kRulePolicyBlocklist;
|
||||
case SNTRuleStateSilentBlock: return kRulePolicySilentBlocklist;
|
||||
case SNTRuleStateRemove: return kRulePolicyRemove;
|
||||
case SNTRuleStateAllowTransitive: return @"AllowTransitive";
|
||||
// This should never be hit. But is here for completion.
|
||||
default: return @"Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)ruleTypeToString:(SNTRuleType)ruleType {
|
||||
switch (ruleType) {
|
||||
case SNTRuleTypeBinary: return kRuleTypeBinary;
|
||||
case SNTRuleTypeCertificate: return kRuleTypeCertificate;
|
||||
case SNTRuleTypeTeamID: return kRuleTypeTeamID;
|
||||
case SNTRuleTypeSigningID: return kRuleTypeSigningID;
|
||||
// This should never be hit. If we have rule types of Unknown then there's a
|
||||
// coding error somewhere.
|
||||
default: return @"Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an NSDictionary representation of the rule. Primarily use for
|
||||
// exporting rules.
|
||||
- (NSDictionary *)dictionaryRepresentation {
|
||||
return @{
|
||||
kRuleIdentifier : self.identifier,
|
||||
kRulePolicy : [self ruleStateToPolicyString:self.state],
|
||||
kRuleType : [self ruleTypeToString:self.type],
|
||||
kRuleCustomMsg : self.customMsg ?: @"",
|
||||
kRuleCustomURL : self.customURL ?: @""
|
||||
};
|
||||
}
|
||||
|
||||
#undef DECODE
|
||||
#undef ENCODE
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
#include "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTSyncConstants.h"
|
||||
|
||||
#import "Source/common/SNTRule.h"
|
||||
|
||||
@@ -95,12 +96,14 @@
|
||||
@"policy" : @"ALLOWLIST",
|
||||
@"rule_type" : @"TEAMID",
|
||||
@"custom_msg" : @"A custom block message",
|
||||
@"custom_url" : @"https://example.com",
|
||||
}];
|
||||
XCTAssertNotNil(sut);
|
||||
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
|
||||
XCTAssertEqual(sut.type, SNTRuleTypeTeamID);
|
||||
XCTAssertEqual(sut.state, SNTRuleStateAllow);
|
||||
XCTAssertEqualObjects(sut.customMsg, @"A custom block message");
|
||||
XCTAssertEqualObjects(sut.customURL, @"https://example.com");
|
||||
|
||||
// TeamIDs must be 10 chars in length
|
||||
sut = [[SNTRule alloc] initWithDictionary:@{
|
||||
@@ -222,4 +225,63 @@
|
||||
XCTAssertNil(sut);
|
||||
}
|
||||
|
||||
- (void)testRuleDictionaryRepresentation {
|
||||
NSDictionary *expectedTeamID = @{
|
||||
@"identifier" : @"ABCDEFGHIJ",
|
||||
@"policy" : @"ALLOWLIST",
|
||||
@"rule_type" : @"TEAMID",
|
||||
@"custom_msg" : @"A custom block message",
|
||||
@"custom_url" : @"https://example.com",
|
||||
};
|
||||
|
||||
SNTRule *sut = [[SNTRule alloc] initWithDictionary:expectedTeamID];
|
||||
NSDictionary *dict = [sut dictionaryRepresentation];
|
||||
XCTAssertEqualObjects(expectedTeamID, dict);
|
||||
|
||||
NSDictionary *expectedBinary = @{
|
||||
@"identifier" : @"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6",
|
||||
@"policy" : @"BLOCKLIST",
|
||||
@"rule_type" : @"BINARY",
|
||||
@"custom_msg" : @"",
|
||||
@"custom_url" : @"",
|
||||
};
|
||||
|
||||
sut = [[SNTRule alloc] initWithDictionary:expectedBinary];
|
||||
dict = [sut dictionaryRepresentation];
|
||||
|
||||
XCTAssertEqualObjects(expectedBinary, dict);
|
||||
}
|
||||
|
||||
- (void)testRuleStateToPolicyString {
|
||||
NSDictionary *expected = @{
|
||||
@"identifier" : @"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6",
|
||||
@"policy" : @"ALLOWLIST",
|
||||
@"rule_type" : @"BINARY",
|
||||
@"custom_msg" : @"A custom block message",
|
||||
@"custom_url" : @"https://example.com",
|
||||
};
|
||||
|
||||
SNTRule *sut = [[SNTRule alloc] initWithDictionary:expected];
|
||||
sut.state = SNTRuleStateBlock;
|
||||
XCTAssertEqualObjects(kRulePolicyBlocklist, [sut dictionaryRepresentation][kRulePolicy]);
|
||||
sut.state = SNTRuleStateSilentBlock;
|
||||
XCTAssertEqualObjects(kRulePolicySilentBlocklist, [sut dictionaryRepresentation][kRulePolicy]);
|
||||
sut.state = SNTRuleStateAllow;
|
||||
XCTAssertEqualObjects(kRulePolicyAllowlist, [sut dictionaryRepresentation][kRulePolicy]);
|
||||
sut.state = SNTRuleStateAllowCompiler;
|
||||
XCTAssertEqualObjects(kRulePolicyAllowlistCompiler, [sut dictionaryRepresentation][kRulePolicy]);
|
||||
// Invalid states
|
||||
sut.state = SNTRuleStateRemove;
|
||||
XCTAssertEqualObjects(kRulePolicyRemove, [sut dictionaryRepresentation][kRulePolicy]);
|
||||
}
|
||||
|
||||
/*
|
||||
- (void)testRuleTypeToString {
|
||||
SNTRule *sut = [[SNTRule alloc] init];
|
||||
XCTAssertEqual(kRuleTypeBinary, [sut ruleTypeToString:@""]);//SNTRuleTypeBinary]);
|
||||
XCTAssertEqual(kRuleTypeCertificate,[sut ruleTypeToString:SNTRuleTypeCertificate]);
|
||||
XCTAssertEqual(kRuleTypeTeamID, [sut ruleTypeToString:SNTRuleTypeTeamID]);
|
||||
XCTAssertEqual(kRuleTypeSigningID,[sut ruleTypeToString:SNTRuleTypeSigningID]);
|
||||
}*/
|
||||
|
||||
@end
|
||||
|
||||
@@ -54,6 +54,7 @@ extern NSString *const kEnableTransitiveRulesDeprecated;
|
||||
extern NSString *const kEnableTransitiveRulesSuperDeprecated;
|
||||
extern NSString *const kEnableAllEventUpload;
|
||||
extern NSString *const kDisableUnknownEventUpload;
|
||||
extern NSString *const kOverrideFileAccessAction;
|
||||
|
||||
extern NSString *const kEvents;
|
||||
extern NSString *const kFileSHA256;
|
||||
@@ -136,6 +137,9 @@ extern NSString *const kLogSync;
|
||||
|
||||
extern const NSUInteger kDefaultEventBatchSize;
|
||||
|
||||
extern NSString *const kPostflightRulesReceived;
|
||||
extern NSString *const kPostflightRulesProcessed;
|
||||
|
||||
///
|
||||
/// kDefaultFullSyncInterval
|
||||
/// kDefaultFCMFullSyncInterval
|
||||
|
||||
@@ -47,6 +47,7 @@ NSString *const kFullSyncInterval = @"full_sync_interval";
|
||||
NSString *const kFCMToken = @"fcm_token";
|
||||
NSString *const kFCMFullSyncInterval = @"fcm_full_sync_interval";
|
||||
NSString *const kFCMGlobalRuleSyncDeadline = @"fcm_global_rule_sync_deadline";
|
||||
NSString *const kOverrideFileAccessAction = @"override_file_access_action";
|
||||
|
||||
NSString *const kEnableBundles = @"enable_bundles";
|
||||
NSString *const kEnableBundlesDeprecated = @"bundles_enabled";
|
||||
@@ -135,6 +136,9 @@ NSString *const kRuleSync = @"rule_sync";
|
||||
NSString *const kConfigSync = @"config_sync";
|
||||
NSString *const kLogSync = @"log_sync";
|
||||
|
||||
NSString *const kPostflightRulesReceived = @"rules_received";
|
||||
NSString *const kPostflightRulesProcessed = @"rules_processed";
|
||||
|
||||
const NSUInteger kDefaultEventBatchSize = 50;
|
||||
const NSUInteger kDefaultFullSyncInterval = 600;
|
||||
const NSUInteger kDefaultPushNotificationsFullSyncInterval = 14400;
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
reply:(void (^)(SNTRule *))reply;
|
||||
- (void)retrieveAllRules:(void (^)(NSArray<SNTRule *> *rules, NSError *error))reply;
|
||||
|
||||
///
|
||||
/// Config ops
|
||||
@@ -53,6 +54,7 @@
|
||||
- (void)setEnableTransitiveRules:(BOOL)enabled reply:(void (^)(void))reply;
|
||||
- (void)setEnableAllEventUpload:(BOOL)enabled reply:(void (^)(void))reply;
|
||||
- (void)setDisableUnknownEventUpload:(BOOL)enabled reply:(void (^)(void))reply;
|
||||
- (void)setOverrideFileAccessAction:(NSString *)action reply:(void (^)(void))reply;
|
||||
|
||||
///
|
||||
/// Syncd Ops
|
||||
|
||||
@@ -53,6 +53,11 @@ NSString *const kBundleID = @"com.google.santa.daemon";
|
||||
forSelector:@selector(databaseRuleAddRules:cleanSlate:reply:)
|
||||
argumentIndex:0
|
||||
ofReply:NO];
|
||||
|
||||
[r setClasses:[NSSet setWithObjects:[NSArray class], [SNTRule class], nil]
|
||||
forSelector:@selector(retrieveAllRules:)
|
||||
argumentIndex:0
|
||||
ofReply:YES];
|
||||
}
|
||||
|
||||
+ (NSXPCInterface *)controlInterface {
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
andCustomURL:(NSString *)url;
|
||||
- (void)postUSBBlockNotification:(SNTDeviceEvent *)event withCustomMessage:(NSString *)message;
|
||||
- (void)postFileAccessBlockNotification:(SNTFileAccessEvent *)event
|
||||
withCustomMessage:(NSString *)message API_AVAILABLE(macos(13.0));
|
||||
customMessage:(NSString *)message
|
||||
customURL:(NSString *)url
|
||||
customText:(NSString *)text API_AVAILABLE(macos(13.0));
|
||||
- (void)postClientModeNotification:(SNTClientMode)clientmode;
|
||||
- (void)postRuleSyncNotificationWithCustomMessage:(NSString *)message;
|
||||
- (void)updateCountsForEvent:(SNTStoredEvent *)event
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -38,6 +39,15 @@ static inline NSString *StringToNSString(const char *str) {
|
||||
return [NSString stringWithUTF8String:str];
|
||||
}
|
||||
|
||||
static inline NSString *OptionalStringToNSString(const std::optional<std::string> &optional_str) {
|
||||
std::string str = optional_str.value_or("");
|
||||
if (str.length() == 0) {
|
||||
return nil;
|
||||
} else {
|
||||
return StringToNSString(str);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace santa::common
|
||||
|
||||
#endif
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#define NOBODY_UID ((unsigned int)-2)
|
||||
#define NOGROUP_GID ((unsigned int)-1)
|
||||
|
||||
@@ -38,6 +40,10 @@
|
||||
// Pretty print C++ string match errors
|
||||
#define XCTAssertCppStringEqual(got, want) XCTAssertCStringEqual((got).c_str(), (want).c_str())
|
||||
|
||||
#define XCTAssertCppStringBeginsWith(got, want) \
|
||||
XCTAssertTrue((got).rfind((want), 0) == 0, "\nPrefix not found.\n\t got: %s\n\twant: %s\n", \
|
||||
(got).c_str(), (want).c_str())
|
||||
|
||||
// Note: Delta between local formatter and the one run on Github. Disable for now.
|
||||
// clang-format off
|
||||
#define XCTAssertSemaTrue(s, sec, m) \
|
||||
|
||||
@@ -381,6 +381,11 @@ message Unlink {
|
||||
optional FileInfo target = 2;
|
||||
}
|
||||
|
||||
// Information about a processes codesigning invalidation event
|
||||
message CodesigningInvalidated {
|
||||
optional ProcessInfoLight instigator = 1;
|
||||
}
|
||||
|
||||
// Information about a link event
|
||||
message Link {
|
||||
// The process performing the link
|
||||
@@ -429,6 +434,9 @@ message Disk {
|
||||
|
||||
// Time device appeared/disappeared
|
||||
optional google.protobuf.Timestamp appearance = 10;
|
||||
|
||||
// Path mounted from
|
||||
optional string mount_from = 11;
|
||||
}
|
||||
|
||||
// Information emitted when Santa captures bundle information
|
||||
@@ -526,6 +534,7 @@ message SantaMessage {
|
||||
Bundle bundle = 19;
|
||||
Allowlist allowlist = 20;
|
||||
FileAccess file_access = 21;
|
||||
CodesigningInvalidated codesigning_invalidated = 22;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ objc_library(
|
||||
":SNTAboutWindowView",
|
||||
":SNTDeviceMessageWindowView",
|
||||
":SNTFileAccessMessageWindowView",
|
||||
"//Source/common:CertificateHelpers",
|
||||
"//Source/common:SNTBlockMessage_SantaGUI",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDeviceEvent",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import <SecurityInterface/SFCertificatePanel.h>
|
||||
|
||||
#import "Source/common/CertificateHelpers.h"
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
@@ -84,7 +85,10 @@
|
||||
|
||||
if (!url) {
|
||||
[self.openEventButton removeFromSuperview];
|
||||
} else {
|
||||
} else if (self.customURL.length == 0) {
|
||||
// Set the button text only if a per-rule custom URL is not used. If a
|
||||
// custom URL is used, it is assumed that the `EventDetailText` config value
|
||||
// does not apply and the default text will be used.
|
||||
NSString *eventDetailText = [[SNTConfigurator configurator] eventDetailText];
|
||||
if (eventDetailText) {
|
||||
[self.openEventButton setTitle:eventDetailText];
|
||||
@@ -114,16 +118,11 @@
|
||||
|
||||
- (IBAction)showCertInfo:(id)sender {
|
||||
// SFCertificatePanel expects an NSArray of SecCertificateRef's
|
||||
NSMutableArray *certArray = [NSMutableArray arrayWithCapacity:[self.event.signingChain count]];
|
||||
for (MOLCertificate *cert in self.event.signingChain) {
|
||||
[certArray addObject:(id)cert.certRef];
|
||||
}
|
||||
|
||||
[[[SFCertificatePanel alloc] init] beginSheetForWindow:self.window
|
||||
modalDelegate:nil
|
||||
didEndSelector:nil
|
||||
contextInfo:nil
|
||||
certificates:certArray
|
||||
certificates:CertificateChain(self.event.signingChain)
|
||||
showGroup:YES];
|
||||
}
|
||||
|
||||
@@ -145,19 +144,7 @@
|
||||
}
|
||||
|
||||
- (NSString *)publisherInfo {
|
||||
MOLCertificate *leafCert = [self.event.signingChain firstObject];
|
||||
|
||||
if ([leafCert.commonName isEqualToString:@"Apple Mac OS Application Signing"]) {
|
||||
return [NSString stringWithFormat:@"App Store (Team ID: %@)", self.event.teamID];
|
||||
} else if (leafCert.commonName && leafCert.orgName) {
|
||||
return [NSString stringWithFormat:@"%@ - %@", leafCert.orgName, leafCert.commonName];
|
||||
} else if (leafCert.commonName) {
|
||||
return leafCert.commonName;
|
||||
} else if (leafCert.orgName) {
|
||||
return leafCert.orgName;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
return Publisher(self.event.signingChain, self.event.teamID);
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedCustomMessage {
|
||||
|
||||
@@ -26,7 +26,10 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
API_AVAILABLE(macos(13.0))
|
||||
@interface SNTFileAccessMessageWindowController : SNTMessageWindowController <NSWindowDelegate>
|
||||
|
||||
- (instancetype)initWithEvent:(SNTFileAccessEvent *)event message:(nullable NSString *)message;
|
||||
- (instancetype)initWithEvent:(SNTFileAccessEvent *)event
|
||||
customMessage:(nullable NSString *)message
|
||||
customURL:(nullable NSString *)url
|
||||
customText:(nullable NSString *)text;
|
||||
|
||||
@property(readonly) SNTFileAccessEvent *event;
|
||||
|
||||
|
||||
@@ -21,16 +21,23 @@
|
||||
|
||||
@interface SNTFileAccessMessageWindowController ()
|
||||
@property NSString *customMessage;
|
||||
@property NSString *customURL;
|
||||
@property NSString *customText;
|
||||
@property SNTFileAccessEvent *event;
|
||||
@end
|
||||
|
||||
@implementation SNTFileAccessMessageWindowController
|
||||
|
||||
- (instancetype)initWithEvent:(SNTFileAccessEvent *)event message:(nullable NSString *)message {
|
||||
- (instancetype)initWithEvent:(SNTFileAccessEvent *)event
|
||||
customMessage:(nullable NSString *)message
|
||||
customURL:(nullable NSString *)url
|
||||
customText:(nullable NSString *)text {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_customMessage = message;
|
||||
_event = event;
|
||||
_customMessage = message;
|
||||
_customURL = url;
|
||||
_customText = text;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -40,21 +47,27 @@
|
||||
[self.window orderOut:sender];
|
||||
}
|
||||
|
||||
self.window =
|
||||
[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 0, 0)
|
||||
styleMask:NSWindowStyleMaskClosable | NSWindowStyleMaskTitled
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
self.window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 0, 0)
|
||||
styleMask:NSWindowStyleMaskBorderless
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
|
||||
self.window.contentViewController =
|
||||
[SNTFileAccessMessageWindowViewFactory createWithWindow:self.window
|
||||
event:self.event
|
||||
customMsg:self.attributedCustomMessage];
|
||||
self.window.contentViewController = [SNTFileAccessMessageWindowViewFactory
|
||||
createWithWindow:self.window
|
||||
event:self.event
|
||||
customMessage:self.attributedCustomMessage
|
||||
customURL:[SNTBlockMessage eventDetailURLForFileAccessEvent:self.event
|
||||
customURL:self.customURL]
|
||||
.absoluteString
|
||||
customText:self.customText
|
||||
uiStateCallback:^(BOOL preventNotificationsForADay) {
|
||||
self.silenceFutureNotifications = preventNotificationsForADay;
|
||||
}];
|
||||
|
||||
self.window.delegate = self;
|
||||
|
||||
// Add app to Cmd+Tab and Dock.
|
||||
NSApp.activationPolicy = NSApplicationActivationPolicyRegular;
|
||||
// Make sure app doesn't appear in Cmd+Tab or Dock.
|
||||
NSApp.activationPolicy = NSApplicationActivationPolicyAccessory;
|
||||
|
||||
[super showWindow:sender];
|
||||
}
|
||||
@@ -66,14 +79,17 @@
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedCustomMessage {
|
||||
return [SNTBlockMessage formatMessage:self.customMessage];
|
||||
return [SNTBlockMessage attributedBlockMessageForFileAccessEvent:self.event
|
||||
customMessage: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]];
|
||||
// The hash for display de-duplication/silencing purposes is a combination of:
|
||||
// 1. The current file access rule version
|
||||
// 2. The name of the rule that was violated
|
||||
// 3. The path of the process
|
||||
return [NSString
|
||||
stringWithFormat:@"%@|%@|%@", self.event.ruleVersion, self.event.ruleName, self.event.filePath];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -12,14 +12,25 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
import SecurityInterface
|
||||
import SwiftUI
|
||||
|
||||
import santa_common_SNTFileAccessEvent
|
||||
|
||||
@available(macOS 13, *)
|
||||
@objc public class SNTFileAccessMessageWindowViewFactory : NSObject {
|
||||
@objc public static func createWith(window: NSWindow, event: SNTFileAccessEvent, customMsg: NSAttributedString?) -> NSViewController {
|
||||
return NSHostingController(rootView:SNTFileAccessMessageWindowView(window:window, event:event, customMsg:customMsg)
|
||||
@objc public static func createWith(window: NSWindow,
|
||||
event: SNTFileAccessEvent,
|
||||
customMessage: NSAttributedString?,
|
||||
customURL: NSString?,
|
||||
customText: NSString?,
|
||||
uiStateCallback: ((Bool) -> Void)?) -> NSViewController {
|
||||
return NSHostingController(rootView:SNTFileAccessMessageWindowView(window:window,
|
||||
event:event,
|
||||
customMessage:customMessage,
|
||||
customURL:customURL as String?,
|
||||
customText:customText as String?,
|
||||
uiStateCallback:uiStateCallback)
|
||||
.frame(width:800, height:600))
|
||||
}
|
||||
}
|
||||
@@ -28,16 +39,26 @@ import santa_common_SNTFileAccessEvent
|
||||
struct Property : View {
|
||||
var lbl: String
|
||||
var val: String
|
||||
var propertyAction: (() -> Void)? = nil
|
||||
|
||||
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)
|
||||
HStack {
|
||||
if let block = propertyAction {
|
||||
Button(action: {
|
||||
block()
|
||||
}) {
|
||||
Image(systemName: "info.circle.fill")
|
||||
}.buttonStyle(BorderlessButtonStyle())
|
||||
}
|
||||
Text(lbl + ":")
|
||||
.frame(alignment: .trailing)
|
||||
.lineLimit(1)
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.padding(Edge.Set.horizontal, 10)
|
||||
}.frame(width: width, alignment: .trailing)
|
||||
|
||||
Text(val)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
@@ -50,6 +71,7 @@ struct Property : View {
|
||||
@available(macOS 13, *)
|
||||
struct Event: View {
|
||||
let e: SNTFileAccessEvent
|
||||
let window: NSWindow?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing:10) {
|
||||
@@ -64,6 +86,18 @@ struct Event: View {
|
||||
Property(lbl: "Application", val: app)
|
||||
}
|
||||
|
||||
if let pub = e.publisherInfo {
|
||||
Property(lbl: "Publisher", val: pub) {
|
||||
SFCertificatePanel.shared()
|
||||
.beginSheet(for: window,
|
||||
modalDelegate: nil,
|
||||
didEnd: nil,
|
||||
contextInfo: nil,
|
||||
certificates: e.signingChainCertRefs,
|
||||
showGroup: true)
|
||||
}
|
||||
}
|
||||
|
||||
Property(lbl: "Name", val: (e.filePath as NSString).lastPathComponent)
|
||||
Property(lbl: "Path", val: e.filePath)
|
||||
Property(lbl: "Identifier", val: e.fileSHA256)
|
||||
@@ -76,22 +110,26 @@ struct Event: View {
|
||||
struct SNTFileAccessMessageWindowView: View {
|
||||
let window: NSWindow?
|
||||
let event: SNTFileAccessEvent?
|
||||
let customMsg: NSAttributedString?
|
||||
let customMessage: NSAttributedString?
|
||||
let customURL: String?
|
||||
let customText: String?
|
||||
let uiStateCallback: ((Bool) -> Void)?
|
||||
|
||||
@State private var checked = false
|
||||
@Environment(\.openURL) var openURL
|
||||
@State public var checked = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing:20.0) {
|
||||
Spacer()
|
||||
Text("Santa").font(Font.custom("HelveticaNeue-UltraLight", size: 34.0))
|
||||
|
||||
if let msg = customMsg {
|
||||
if let msg = customMessage {
|
||||
Text(AttributedString(msg)).multilineTextAlignment(.center).padding(15.0)
|
||||
} else {
|
||||
Text("Access to a protected resource was denied.").multilineTextAlignment(.center).padding(15.0)
|
||||
}
|
||||
|
||||
Event(e: event!)
|
||||
Event(e: event!, window: window)
|
||||
|
||||
Toggle(isOn: $checked) {
|
||||
Text("Prevent future notifications for this application for a day")
|
||||
@@ -99,9 +137,12 @@ struct SNTFileAccessMessageWindowView: View {
|
||||
}
|
||||
|
||||
VStack(spacing:15) {
|
||||
Button(action: openButton, label: {
|
||||
Text("Open Event Info...").frame(maxWidth:.infinity)
|
||||
})
|
||||
if customURL != nil {
|
||||
Button(action: openButton, label: {
|
||||
|
||||
Text(customText ?? "Open Event...").frame(maxWidth:.infinity)
|
||||
})
|
||||
}
|
||||
Button(action: dismissButton, label: {
|
||||
Text("Dismiss").frame(maxWidth:.infinity)
|
||||
})
|
||||
@@ -113,19 +154,25 @@ struct SNTFileAccessMessageWindowView: View {
|
||||
}.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...")
|
||||
guard let urlString = customURL else {
|
||||
print("No URL available")
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = URL(string: urlString) else {
|
||||
print("Failed to create URL")
|
||||
return
|
||||
}
|
||||
|
||||
openURL(url)
|
||||
}
|
||||
|
||||
func dismissButton() {
|
||||
if let block = uiStateCallback {
|
||||
block(self.checked)
|
||||
}
|
||||
window?.close()
|
||||
print("close window")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +200,11 @@ func testFileAccessEvent() -> SNTFileAccessEvent {
|
||||
@available(macOS 13, *)
|
||||
struct SNTFileAccessMessageWindowView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SNTFileAccessMessageWindowView(window: nil, event: testFileAccessEvent(), customMsg: nil)
|
||||
SNTFileAccessMessageWindowView(window: nil,
|
||||
event: testFileAccessEvent(),
|
||||
customMessage: nil,
|
||||
customURL: nil,
|
||||
customText: nil,
|
||||
uiStateCallback: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,14 +349,19 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
|
||||
}
|
||||
|
||||
- (void)postFileAccessBlockNotification:(SNTFileAccessEvent *)event
|
||||
withCustomMessage:(NSString *)message API_AVAILABLE(macos(13.0)) {
|
||||
customMessage:(NSString *)message
|
||||
customURL:(NSString *)url
|
||||
customText:(NSString *)text API_AVAILABLE(macos(13.0)) {
|
||||
if (!event) {
|
||||
LOGI(@"Error: Missing event object in message received from daemon!");
|
||||
return;
|
||||
}
|
||||
|
||||
SNTFileAccessMessageWindowController *pendingMsg =
|
||||
[[SNTFileAccessMessageWindowController alloc] initWithEvent:event message:message];
|
||||
[[SNTFileAccessMessageWindowController alloc] initWithEvent:event
|
||||
customMessage:message
|
||||
customURL:url
|
||||
customText:text];
|
||||
|
||||
[self queueMessage:pendingMsg];
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ objc_library(
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:santa_cc_proto_library_wrapper",
|
||||
"//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:binaryproto_cc_proto_library_wrapper",
|
||||
"@com_google_protobuf//src/google/protobuf/json",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -114,6 +115,8 @@ santa_unit_test(
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SNTXPCBundleServiceInterface",
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
"@MOLCertificate",
|
||||
"@MOLCodesignChecker",
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/common/SNTXPCBundleServiceInterface.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/santactl/SNTCommand.h"
|
||||
#import "Source/santactl/SNTCommandController.h"
|
||||
@@ -55,6 +57,13 @@ static NSString *const kValidUntil = @"Valid Until";
|
||||
static NSString *const kSHA256 = @"SHA-256";
|
||||
static NSString *const kSHA1 = @"SHA-1";
|
||||
|
||||
// bundle info keys
|
||||
static NSString *const kBundleInfo = @"Bundle Info";
|
||||
static NSString *const kBundlePath = @"Main Bundle Path";
|
||||
static NSString *const kBundleID = @"Main Bundle ID";
|
||||
static NSString *const kBundleHash = @"Bundle Hash";
|
||||
static NSString *const kBundleHashes = @"Bundle Hashes";
|
||||
|
||||
// Message displayed when daemon communication fails
|
||||
static NSString *const kCommunicationErrorMsg = @"Could not communicate with daemon";
|
||||
|
||||
@@ -72,6 +81,7 @@ NSString *formattedStringForKeyArray(NSArray<NSString *> *array) {
|
||||
// Properties set from commandline flags
|
||||
@property(nonatomic) BOOL recursive;
|
||||
@property(nonatomic) BOOL jsonOutput;
|
||||
@property(nonatomic) BOOL bundleInfo;
|
||||
@property(nonatomic) NSNumber *certIndex;
|
||||
@property(nonatomic, copy) NSArray<NSString *> *outputKeyList;
|
||||
@property(nonatomic, copy) NSDictionary<NSString *, NSRegularExpression *> *outputFilters;
|
||||
@@ -156,6 +166,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
@"\n"
|
||||
@"Usage: santactl fileinfo [options] [file-paths]\n"
|
||||
@" --recursive (-r): Search directories recursively.\n"
|
||||
@" Incompatible with --bundleinfo.\n"
|
||||
@" --json: Output in JSON format.\n"
|
||||
@" --key: Search and return this one piece of information.\n"
|
||||
@" You may specify multiple keys by repeating this flag.\n"
|
||||
@@ -167,12 +178,16 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
@" signing chain to show info only for that certificate.\n"
|
||||
@" 0 up to n for the leaf certificate up to the root\n"
|
||||
@" -1 down to -n-1 for the root certificate down to the leaf\n"
|
||||
@" Incompatible with --bundleinfo."
|
||||
@"\n"
|
||||
@" --filter: Use predicates of the form 'key=regex' to filter out which files\n"
|
||||
@" are displayed. Valid keys are the same as for --key. Value is a\n"
|
||||
@" case-insensitive regular expression which must match anywhere in\n"
|
||||
@" the keyed property value for the file's info to be displayed.\n"
|
||||
@" You may specify multiple filters by repeating this flag.\n"
|
||||
@" --bundleinfo: If the file is part of a bundle, will also display bundle\n"
|
||||
@" hash information and hashes of all bundle executables.\n"
|
||||
@" Incompatible with --recursive and --cert-index.\n"
|
||||
@"\n"
|
||||
@"Examples: santactl fileinfo --cert-index 1 --key SHA-256 --json /usr/bin/yes\n"
|
||||
@" santactl fileinfo --key SHA-256 --json /usr/bin/yes\n"
|
||||
@@ -682,6 +697,48 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
if (outputDict[key]) continue; // ignore keys that we've already set due to a filter
|
||||
outputDict[key] = self.propertyMap[key](self, fileInfo);
|
||||
}
|
||||
|
||||
if (self.bundleInfo) {
|
||||
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
|
||||
se.fileBundlePath = fileInfo.bundlePath;
|
||||
|
||||
MOLXPCConnection *bc = [SNTXPCBundleServiceInterface configuredConnection];
|
||||
[bc resume];
|
||||
|
||||
__block NSMutableDictionary *bundleInfo = [[NSMutableDictionary alloc] init];
|
||||
|
||||
bundleInfo[kBundlePath] = fileInfo.bundle.bundlePath;
|
||||
bundleInfo[kBundleID] = fileInfo.bundle.bundleIdentifier;
|
||||
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
[[bc remoteObjectProxy]
|
||||
hashBundleBinariesForEvent:se
|
||||
reply:^(NSString *hash, NSArray<SNTStoredEvent *> *events,
|
||||
NSNumber *time) {
|
||||
bundleInfo[kBundleHash] = hash;
|
||||
|
||||
NSMutableArray *bundleHashes = [[NSMutableArray alloc] init];
|
||||
|
||||
for (SNTStoredEvent *event in events) {
|
||||
[bundleHashes
|
||||
addObject:@{kSHA256 : event.fileSHA256, kPath : event.filePath}];
|
||||
}
|
||||
|
||||
bundleInfo[kBundleHashes] = bundleHashes;
|
||||
[[bc remoteObjectProxy] spindown];
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
|
||||
int secondsToWait = 30;
|
||||
if (dispatch_semaphore_wait(sema,
|
||||
dispatch_time(DISPATCH_TIME_NOW, secondsToWait * NSEC_PER_SEC))) {
|
||||
fprintf(stderr, "The bundle service did not finish collecting hashes within %d seconds\n",
|
||||
secondsToWait);
|
||||
}
|
||||
|
||||
outputDict[kBundleInfo] = bundleInfo;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's nothing in the outputDict, then don't need to print anything.
|
||||
@@ -710,6 +767,11 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (self.bundleInfo) {
|
||||
[output appendString:[self stringForBundleInfo:outputDict[kBundleInfo] key:kBundleInfo]];
|
||||
}
|
||||
|
||||
if (!singleKey) [output appendString:@"\n"];
|
||||
}
|
||||
|
||||
@@ -739,6 +801,9 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
if ([arg caseInsensitiveCompare:@"--json"] == NSOrderedSame) {
|
||||
self.jsonOutput = YES;
|
||||
} else if ([arg caseInsensitiveCompare:@"--cert-index"] == NSOrderedSame) {
|
||||
if (self.bundleInfo) {
|
||||
[self printErrorUsageAndExit:@"\n--cert-index is incompatible with --bundleinfo"];
|
||||
}
|
||||
i += 1; // advance to next argument and grab index
|
||||
if (i >= nargs || [arguments[i] hasPrefix:@"--"]) {
|
||||
[self printErrorUsageAndExit:@"\n--cert-index requires an argument"];
|
||||
@@ -788,7 +853,17 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
filters[key] = regex;
|
||||
} else if ([arg caseInsensitiveCompare:@"--recursive"] == NSOrderedSame ||
|
||||
[arg caseInsensitiveCompare:@"-r"] == NSOrderedSame) {
|
||||
if (self.bundleInfo) {
|
||||
[self printErrorUsageAndExit:@"\n--recursive is incompatible with --bundleinfo"];
|
||||
}
|
||||
self.recursive = YES;
|
||||
} else if ([arg caseInsensitiveCompare:@"--bundleinfo"] == NSOrderedSame ||
|
||||
[arg caseInsensitiveCompare:@"-b"] == NSOrderedSame) {
|
||||
if (self.recursive || self.certIndex) {
|
||||
[self printErrorUsageAndExit:
|
||||
@"\n--bundleinfo is incompatible with --recursive and --cert-index"];
|
||||
}
|
||||
self.bundleInfo = YES;
|
||||
} else {
|
||||
[paths addObject:arg];
|
||||
}
|
||||
@@ -868,6 +943,22 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
- (NSString *)stringForBundleInfo:(NSDictionary *)bundleInfo key:(NSString *)key {
|
||||
NSMutableString *result = [NSMutableString string];
|
||||
|
||||
[result appendFormat:@"%@:\n", key];
|
||||
|
||||
[result appendFormat:@" %-20s: %@\n", kBundlePath.UTF8String, bundleInfo[kBundlePath]];
|
||||
[result appendFormat:@" %-20s: %@\n", kBundleID.UTF8String, bundleInfo[kBundleID]];
|
||||
[result appendFormat:@" %-20s: %@\n", kBundleHash.UTF8String, bundleInfo[kBundleHash]];
|
||||
|
||||
for (NSDictionary *hashPath in bundleInfo[kBundleHashes]) {
|
||||
[result appendFormat:@" %@ %@\n", hashPath[kSHA256], hashPath[kPath]];
|
||||
}
|
||||
|
||||
return [result copy];
|
||||
}
|
||||
|
||||
- (NSString *)stringForCertificate:(NSDictionary *)cert withKeys:(NSArray *)keys index:(int)index {
|
||||
if (!cert) return @"";
|
||||
NSMutableString *result = [NSMutableString string];
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <google/protobuf/util/json_util.h>
|
||||
#include <google/protobuf/json/json.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <iostream>
|
||||
@@ -26,8 +26,8 @@
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/binaryproto_proto_include_wrapper.h"
|
||||
#include "google/protobuf/any.pb.h"
|
||||
|
||||
using google::protobuf::util::JsonPrintOptions;
|
||||
using google::protobuf::util::MessageToJsonString;
|
||||
using JsonPrintOptions = google::protobuf::json::PrintOptions;
|
||||
using google::protobuf::json::MessageToJsonString;
|
||||
using santa::fsspool::binaryproto::LogBatch;
|
||||
namespace pbv1 = ::santa::pb::v1;
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
@" --compiler: allow and mark as a compiler\n"
|
||||
@" --remove: remove existing rule\n"
|
||||
@" --check: check for an existing rule\n"
|
||||
@" --import {path}: import rules from a JSON file\n"
|
||||
@" --export {path}: export rules to a JSON file\n"
|
||||
@"\n"
|
||||
@" One of:\n"
|
||||
@" --path {path}: path of binary/bundle to add/remove.\n"
|
||||
@@ -81,7 +83,21 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
@" that the signing ID is properly scoped to a developer. For the special\n"
|
||||
@" case of platform binaries, `TeamID` should be replaced with the string\n"
|
||||
@" \"platform\" (e.g. `platform:SigningID`). This allows for rules\n"
|
||||
@" targeting Apple-signed binaries that do not have a team ID.\n");
|
||||
@" targeting Apple-signed binaries that do not have a team ID.\n"
|
||||
@"\n"
|
||||
@" Importing / Exporting Rules:\n"
|
||||
@" If santa is not configured to use a sync server one can export\n"
|
||||
@" & import its non-static rules to and from JSON files using the \n"
|
||||
@" --export/--import flags. These files have the following form:\n"
|
||||
@"\n"
|
||||
@" {\"rules\": [{rule-dictionaries}]}\n"
|
||||
@" e.g. {\"rules\": [\n"
|
||||
@" {\"policy\": \"BLOCKLIST\",\n"
|
||||
@" \"identifier\": "
|
||||
@"\"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6\"\n"
|
||||
@" \"custom_url\" : \"\",\n"
|
||||
@" \"custom_msg\": \"/bin/ls block for demo\"}\n"
|
||||
@" ]}\n");
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
@@ -103,7 +119,10 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
newRule.type = SNTRuleTypeBinary;
|
||||
|
||||
NSString *path;
|
||||
NSString *jsonFilePath;
|
||||
BOOL check = NO;
|
||||
BOOL importRules = NO;
|
||||
BOOL exportRules = NO;
|
||||
|
||||
// Parse arguments
|
||||
for (NSUInteger i = 0; i < arguments.count; ++i) {
|
||||
@@ -154,11 +173,48 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
} else if ([arg caseInsensitiveCompare:@"--force"] == NSOrderedSame) {
|
||||
// Don't do anything special.
|
||||
#endif
|
||||
} else if ([arg caseInsensitiveCompare:@"--import"] == NSOrderedSame) {
|
||||
if (exportRules) {
|
||||
[self printErrorUsageAndExit:@"--import and --export are mutually exclusive"];
|
||||
}
|
||||
importRules = YES;
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"--import requires an argument"];
|
||||
}
|
||||
jsonFilePath = arguments[i];
|
||||
} else if ([arg caseInsensitiveCompare:@"--export"] == NSOrderedSame) {
|
||||
if (importRules) {
|
||||
[self printErrorUsageAndExit:@"--import and --export are mutually exclusive"];
|
||||
}
|
||||
exportRules = YES;
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"--export requires an argument"];
|
||||
}
|
||||
jsonFilePath = arguments[i];
|
||||
} else if ([arg caseInsensitiveCompare:@"--help"] == NSOrderedSame ||
|
||||
[arg caseInsensitiveCompare:@"-h"] == NSOrderedSame) {
|
||||
printf("%s\n", self.class.longHelpText.UTF8String);
|
||||
exit(0);
|
||||
} else {
|
||||
[self printErrorUsageAndExit:[@"Unknown argument: " stringByAppendingString:arg]];
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonFilePath.length > 0) {
|
||||
if (importRules) {
|
||||
if (newRule.identifier != nil || path != nil || check) {
|
||||
[self printErrorUsageAndExit:@"--import can only be used by itself"];
|
||||
}
|
||||
[self importJSONFile:jsonFilePath];
|
||||
} else if (exportRules) {
|
||||
if (newRule.identifier != nil || path != nil || check) {
|
||||
[self printErrorUsageAndExit:@"--export can only be used by itself"];
|
||||
}
|
||||
[self exportJSONFile:jsonFilePath];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (path) {
|
||||
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithPath:path];
|
||||
if (!fi.path) {
|
||||
@@ -302,4 +358,101 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
exit(0);
|
||||
}
|
||||
|
||||
- (void)importJSONFile:(NSString *)jsonFilePath {
|
||||
// If the file exists parse it and then add the rules one at a time.
|
||||
NSError *error;
|
||||
NSData *data = [NSData dataWithContentsOfFile:jsonFilePath options:0 error:&error];
|
||||
if (error) {
|
||||
[self printErrorUsageAndExit:[NSString stringWithFormat:@"Failed to read %@: %@", jsonFilePath,
|
||||
error.localizedDescription]];
|
||||
}
|
||||
|
||||
// We expect a JSON object with one key "rules". This is an array of rule
|
||||
// objects.
|
||||
// e.g.
|
||||
// {"rules": [{
|
||||
// "policy" : "BLOCKLIST",
|
||||
// "rule_type" : "BINARY",
|
||||
// "identifier" : "84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6"
|
||||
// "custom_url" : "",
|
||||
// "custom_msg" : "/bin/ls block for demo"
|
||||
// }]}
|
||||
NSDictionary *rules = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
|
||||
if (error) {
|
||||
[self printErrorUsageAndExit:[NSString stringWithFormat:@"Failed to parse %@: %@", jsonFilePath,
|
||||
error.localizedDescription]];
|
||||
}
|
||||
|
||||
NSMutableArray<SNTRule *> *parsedRules = [[NSMutableArray alloc] init];
|
||||
|
||||
for (NSDictionary *jsonRule in rules[@"rules"]) {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:jsonRule];
|
||||
if (!rule) {
|
||||
[self printErrorUsageAndExit:[NSString stringWithFormat:@"Invalid rule: %@", jsonRule]];
|
||||
}
|
||||
[parsedRules addObject:rule];
|
||||
}
|
||||
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
databaseRuleAddRules:parsedRules
|
||||
cleanSlate:NO
|
||||
reply:^(NSError *error) {
|
||||
if (error) {
|
||||
printf("Failed to modify rules: %s",
|
||||
[error.localizedDescription UTF8String]);
|
||||
LOGD(@"Failure reason: %@", error.localizedFailureReason);
|
||||
exit(1);
|
||||
}
|
||||
exit(0);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)exportJSONFile:(NSString *)jsonFilePath {
|
||||
// Get the rules from the daemon and then write them to the file.
|
||||
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
|
||||
[rop retrieveAllRules:^(NSArray<SNTRule *> *rules, NSError *error) {
|
||||
if (error) {
|
||||
printf("Failed to get rules: %s", [error.localizedDescription UTF8String]);
|
||||
LOGD(@"Failure reason: %@", error.localizedFailureReason);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (rules.count == 0) {
|
||||
printf("No rules to export.\n");
|
||||
exit(1);
|
||||
}
|
||||
// Convert Rules to an NSDictionary.
|
||||
NSMutableArray *rulesAsDicts = [[NSMutableArray alloc] init];
|
||||
|
||||
for (SNTRule *rule in rules) {
|
||||
// Omit transitive and remove rules as they're not relevan.
|
||||
if (rule.state == SNTRuleStateAllowTransitive || rule.state == SNTRuleStateRemove) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[rulesAsDicts addObject:[rule dictionaryRepresentation]];
|
||||
}
|
||||
|
||||
NSOutputStream *outputStream = [[NSOutputStream alloc] initToFileAtPath:jsonFilePath append:NO];
|
||||
[outputStream open];
|
||||
|
||||
// Write the rules to the file.
|
||||
// File should look like the following JSON:
|
||||
// {"rules": [{"policy": "ALLOWLIST", "identifier": hash, "rule_type: "BINARY"},}]}
|
||||
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:@{@"rules" : rulesAsDicts}
|
||||
options:NSJSONWritingPrettyPrinted
|
||||
error:&error];
|
||||
// Print error
|
||||
if (error) {
|
||||
printf("Failed to jsonify rules: %s", [error.localizedDescription UTF8String]);
|
||||
LOGD(@"Failure reason: %@", error.localizedFailureReason);
|
||||
exit(1);
|
||||
}
|
||||
// Write jsonData to the file
|
||||
[outputStream write:jsonData.bytes maxLength:jsonData.length];
|
||||
[outputStream close];
|
||||
exit(0);
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -15,11 +15,22 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/santactl/SNTCommand.h"
|
||||
#import "Source/santactl/SNTCommandController.h"
|
||||
|
||||
NSString *StartupOptionToString(SNTDeviceManagerStartupPreferences pref) {
|
||||
switch (pref) {
|
||||
case SNTDeviceManagerStartupPreferencesUnmount: return @"Unmount";
|
||||
case SNTDeviceManagerStartupPreferencesForceUnmount: return @"ForceUnmount";
|
||||
case SNTDeviceManagerStartupPreferencesRemount: return @"Remount";
|
||||
case SNTDeviceManagerStartupPreferencesForceRemount: return @"ForceRemount";
|
||||
default: return @"None";
|
||||
}
|
||||
}
|
||||
|
||||
@interface SNTCommandStatus : SNTCommand <SNTCommandProtocol>
|
||||
@end
|
||||
|
||||
@@ -195,6 +206,7 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"remount_usb_mode" : (configurator.blockUSBMount && configurator.remountUSBMode.count
|
||||
? configurator.remountUSBMode
|
||||
: @""),
|
||||
@"on_start_usb_options" : StartupOptionToString(configurator.onStartUSBOptions),
|
||||
},
|
||||
@"database" : @{
|
||||
@"binary_rules" : @(binaryRuleCount),
|
||||
@@ -252,9 +264,11 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
printf(" %-25s | %s\n", "File Logging", (fileLogging ? "Yes" : "No"));
|
||||
printf(" %-25s | %s\n", "USB Blocking", (configurator.blockUSBMount ? "Yes" : "No"));
|
||||
if (configurator.blockUSBMount && configurator.remountUSBMode.count > 0) {
|
||||
printf(" %-25s | %s\n", "USB Remounting Mode:",
|
||||
printf(" %-25s | %s\n", "USB Remounting Mode",
|
||||
[[configurator.remountUSBMode componentsJoinedByString:@", "] UTF8String]);
|
||||
}
|
||||
printf(" %-25s | %s\n", "On Start USB Options",
|
||||
StartupOptionToString(configurator.onStartUSBOptions).UTF8String);
|
||||
printf(" %-25s | %lld (Peak: %.2f%%)\n", "Watchdog CPU Events", cpuEvents, cpuPeak);
|
||||
printf(" %-25s | %lld (Peak: %.2fMB)\n", "Watchdog RAM Events", ramEvents, ramPeak);
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ REGISTER_COMMAND_NAME(@"sync")
|
||||
#pragma mark SNTCommand protocol methods
|
||||
|
||||
+ (BOOL)requiresRoot {
|
||||
return YES;
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (BOOL)requiresDaemonConn {
|
||||
|
||||
@@ -213,6 +213,9 @@ objc_library(
|
||||
name = "TTYWriter",
|
||||
srcs = ["TTYWriter.mm"],
|
||||
hdrs = ["TTYWriter.h"],
|
||||
sdk_dylibs = [
|
||||
"EndpointSecurity",
|
||||
],
|
||||
deps = [
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:String",
|
||||
@@ -363,6 +366,7 @@ objc_library(
|
||||
":WatchItemPolicy",
|
||||
":WatchItems",
|
||||
"//Source/common:Platform",
|
||||
"//Source/common:SNTBlockMessage",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTFileAccessEvent",
|
||||
@@ -391,8 +395,10 @@ objc_library(
|
||||
":Metrics",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTDeviceEvent",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -470,6 +476,7 @@ objc_library(
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
"//Source/common:String",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -527,6 +534,8 @@ objc_library(
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:String",
|
||||
"//Source/common:santa_cc_proto_library_wrapper",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_protobuf//src/google/protobuf/json",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -667,6 +676,7 @@ objc_library(
|
||||
hdrs = ["Metrics.h"],
|
||||
deps = [
|
||||
":SNTApplicationCoreMetrics",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTXPCMetricServiceInterface",
|
||||
@@ -885,8 +895,10 @@ santa_unit_test(
|
||||
":Metrics",
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTDatabaseController",
|
||||
":SNTDecisionCache",
|
||||
":SNTEndpointSecurityAuthorizer",
|
||||
":SantadDeps",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:TestUtils",
|
||||
"@MOLCertificate",
|
||||
@@ -985,7 +997,9 @@ santa_unit_test(
|
||||
"//Source/common:TestUtils",
|
||||
"//Source/common:santa_cc_proto_library_wrapper",
|
||||
"@OCMock",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_googletest//:gtest",
|
||||
"@com_google_protobuf//src/google/protobuf/json",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1234,6 +1248,7 @@ santa_unit_test(
|
||||
":WatchItems",
|
||||
"//Source/common:Platform",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:TestUtils",
|
||||
"@MOLCertificate",
|
||||
@@ -1304,6 +1319,7 @@ santa_unit_test(
|
||||
":Metrics",
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTEndpointSecurityDeviceManager",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDeviceEvent",
|
||||
"//Source/common:TestUtils",
|
||||
|
||||
@@ -101,6 +101,11 @@
|
||||
///
|
||||
- (void)removeOutdatedTransitiveRules;
|
||||
|
||||
///
|
||||
/// Retrieve all rules from the database for export.
|
||||
///
|
||||
- (NSArray<SNTRule *> *)retrieveAllRules;
|
||||
|
||||
///
|
||||
/// A map of a file hashes to cached decisions. This is used to pre-validate and whitelist
|
||||
/// certain critical system binaries that are integral to Santa's functionality.
|
||||
|
||||
@@ -539,4 +539,19 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark Querying
|
||||
|
||||
// Retrieve all rules from the Database
|
||||
- (NSArray<SNTRule *> *)retrieveAllRules {
|
||||
NSMutableArray<SNTRule *> *rules = [NSMutableArray array];
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
FMResultSet *rs = [db executeQuery:@"SELECT * FROM rules"];
|
||||
while ([rs next]) {
|
||||
[rules addObject:[self ruleFromResultSet:rs]];
|
||||
}
|
||||
[rs close];
|
||||
}];
|
||||
return rules;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -301,4 +301,25 @@
|
||||
[[NSFileManager defaultManager] removeItemAtPath:dbPath error:NULL];
|
||||
}
|
||||
|
||||
- (void)testRetrieveAllRulesWithEmptyDatabase {
|
||||
NSArray<SNTRule *> *rules = [self.sut retrieveAllRules];
|
||||
XCTAssertEqual(rules.count, 0);
|
||||
}
|
||||
|
||||
- (void)testRetrieveAllRulesWithMultipleRules {
|
||||
[self.sut addRules:@[
|
||||
[self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO]
|
||||
]
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
|
||||
NSArray<SNTRule *> *rules = [self.sut retrieveAllRules];
|
||||
XCTAssertEqual(rules.count, 4);
|
||||
XCTAssertEqualObjects(rules[0], [self _exampleCertRule]);
|
||||
XCTAssertEqualObjects(rules[1], [self _exampleBinaryRule]);
|
||||
XCTAssertEqualObjects(rules[2], [self _exampleTeamIDRule]);
|
||||
XCTAssertEqualObjects(rules[3], [self _exampleSigningIDRuleIsPlatform:NO]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#ifndef SANTA__SANTAD__DATALAYER_WATCHITEMPOLICY_H
|
||||
#define SANTA__SANTAD__DATALAYER_WATCHITEMPOLICY_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
|
||||
#include <optional>
|
||||
@@ -29,8 +30,7 @@ enum class WatchItemPathType {
|
||||
kLiteral,
|
||||
};
|
||||
|
||||
static constexpr WatchItemPathType kWatchItemPolicyDefaultPathType =
|
||||
WatchItemPathType::kLiteral;
|
||||
static constexpr WatchItemPathType kWatchItemPolicyDefaultPathType = WatchItemPathType::kLiteral;
|
||||
static constexpr bool kWatchItemPolicyDefaultAllowReadAccess = false;
|
||||
static constexpr bool kWatchItemPolicyDefaultAuditOnly = true;
|
||||
static constexpr bool kWatchItemPolicyDefaultInvertProcessExceptions = false;
|
||||
@@ -39,8 +39,8 @@ static constexpr bool kWatchItemPolicyDefaultEnableSilentTTYMode = false;
|
||||
|
||||
struct WatchItemPolicy {
|
||||
struct Process {
|
||||
Process(std::string bp, std::string sid, std::string ti,
|
||||
std::vector<uint8_t> cdh, std::string ch, std::optional<bool> pb)
|
||||
Process(std::string bp, std::string sid, std::string ti, std::vector<uint8_t> cdh,
|
||||
std::string ch, std::optional<bool> pb)
|
||||
: binary_path(bp),
|
||||
signing_id(sid),
|
||||
team_id(ti),
|
||||
@@ -49,13 +49,11 @@ struct WatchItemPolicy {
|
||||
platform_binary(pb) {}
|
||||
|
||||
bool operator==(const Process &other) const {
|
||||
return binary_path == other.binary_path &&
|
||||
signing_id == other.signing_id && team_id == other.team_id &&
|
||||
cdhash == other.cdhash &&
|
||||
return binary_path == other.binary_path && signing_id == other.signing_id &&
|
||||
team_id == other.team_id && cdhash == other.cdhash &&
|
||||
certificate_sha256 == other.certificate_sha256 &&
|
||||
platform_binary.has_value() == other.platform_binary.has_value() &&
|
||||
platform_binary.value_or(false) ==
|
||||
other.platform_binary.value_or(false);
|
||||
platform_binary.value_or(false) == other.platform_binary.value_or(false);
|
||||
}
|
||||
|
||||
bool operator!=(const Process &other) const { return !(*this == other); }
|
||||
@@ -74,8 +72,8 @@ struct WatchItemPolicy {
|
||||
bool ao = kWatchItemPolicyDefaultAuditOnly,
|
||||
bool ipe = kWatchItemPolicyDefaultInvertProcessExceptions,
|
||||
bool esm = kWatchItemPolicyDefaultEnableSilentMode,
|
||||
bool estm = kWatchItemPolicyDefaultEnableSilentTTYMode,
|
||||
std::string_view cm = "", std::vector<Process> procs = {})
|
||||
bool estm = kWatchItemPolicyDefaultEnableSilentTTYMode, std::string_view cm = "",
|
||||
NSString *edu = nil, NSString *edt = nil, std::vector<Process> procs = {})
|
||||
: name(n),
|
||||
path(p),
|
||||
path_type(pt),
|
||||
@@ -84,24 +82,23 @@ struct WatchItemPolicy {
|
||||
invert_process_exceptions(ipe),
|
||||
silent(esm),
|
||||
silent_tty(estm),
|
||||
custom_message(cm.length() == 0 ? std::nullopt
|
||||
: std::make_optional<std::string>(cm)),
|
||||
custom_message(cm.length() == 0 ? std::nullopt : std::make_optional<std::string>(cm)),
|
||||
// Note: Empty string considered valid for event_detail_url to allow rules
|
||||
// overriding global setting in order to hide the button.
|
||||
event_detail_url(edu == nil ? std::nullopt : std::make_optional<NSString *>(edu)),
|
||||
event_detail_text(edt.length == 0 ? std::nullopt : std::make_optional<NSString *>(edt)),
|
||||
processes(std::move(procs)) {}
|
||||
|
||||
bool operator==(const WatchItemPolicy &other) const {
|
||||
// Note: Custom message isn't currently considered for equality purposes
|
||||
return name == other.name && path == other.path &&
|
||||
path_type == other.path_type &&
|
||||
allow_read_access == other.allow_read_access &&
|
||||
audit_only == other.audit_only &&
|
||||
invert_process_exceptions == other.invert_process_exceptions &&
|
||||
silent == other.silent && silent_tty == other.silent_tty &&
|
||||
processes == other.processes;
|
||||
// Note: custom_message, event_detail_url, and event_detail_text are not currently considered
|
||||
// for equality purposes
|
||||
return name == other.name && path == other.path && path_type == other.path_type &&
|
||||
allow_read_access == other.allow_read_access && audit_only == other.audit_only &&
|
||||
invert_process_exceptions == other.invert_process_exceptions && silent == other.silent &&
|
||||
silent_tty == other.silent_tty && processes == other.processes;
|
||||
}
|
||||
|
||||
bool operator!=(const WatchItemPolicy &other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
bool operator!=(const WatchItemPolicy &other) const { return !(*this == other); }
|
||||
|
||||
std::string name;
|
||||
std::string path;
|
||||
@@ -112,6 +109,8 @@ struct WatchItemPolicy {
|
||||
bool silent;
|
||||
bool silent_tty;
|
||||
std::optional<std::string> custom_message;
|
||||
std::optional<NSString *> event_detail_url;
|
||||
std::optional<NSString *> event_detail_text;
|
||||
std::vector<Process> processes;
|
||||
|
||||
// WIP - No current way to control via config
|
||||
|
||||
@@ -96,6 +96,9 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
|
||||
|
||||
std::optional<WatchItemsState> State();
|
||||
|
||||
std::pair<NSString *, NSString *> EventDetailLinkInfo(
|
||||
const std::shared_ptr<WatchItemPolicy> &watch_item);
|
||||
|
||||
friend class santa::santad::data_layer::WatchItemsPeer;
|
||||
|
||||
private:
|
||||
@@ -128,6 +131,8 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
|
||||
std::string policy_version_ ABSL_GUARDED_BY(lock_);
|
||||
std::set<id<SNTEndpointSecurityDynamicEventHandler>> registerd_clients_ ABSL_GUARDED_BY(lock_);
|
||||
bool periodic_task_started_ = false;
|
||||
NSString *policy_event_detail_url_ ABSL_GUARDED_BY(lock_);
|
||||
NSString *policy_event_detail_text_ ABSL_GUARDED_BY(lock_);
|
||||
};
|
||||
|
||||
} // namespace santa::santad::data_layer
|
||||
|
||||
@@ -46,6 +46,8 @@ using santa::santad::data_layer::WatchItemPathType;
|
||||
using santa::santad::data_layer::WatchItemPolicy;
|
||||
|
||||
NSString *const kWatchItemConfigKeyVersion = @"Version";
|
||||
NSString *const kWatchItemConfigKeyEventDetailURL = @"EventDetailURL";
|
||||
NSString *const kWatchItemConfigKeyEventDetailText = @"EventDetailText";
|
||||
NSString *const kWatchItemConfigKeyWatchItems = @"WatchItems";
|
||||
NSString *const kWatchItemConfigKeyPaths = @"Paths";
|
||||
NSString *const kWatchItemConfigKeyPathsPath = @"Path";
|
||||
@@ -57,6 +59,8 @@ NSString *const kWatchItemConfigKeyOptionsInvertProcessExceptions = @"InvertProc
|
||||
NSString *const kWatchItemConfigKeyOptionsEnableSilentMode = @"EnableSilentMode";
|
||||
NSString *const kWatchItemConfigKeyOptionsEnableSilentTTYMode = @"EnableSilentTTYMode";
|
||||
NSString *const kWatchItemConfigKeyOptionsCustomMessage = @"BlockMessage";
|
||||
NSString *const kWatchItemConfigKeyOptionsEventDetailURL = kWatchItemConfigKeyEventDetailURL;
|
||||
NSString *const kWatchItemConfigKeyOptionsEventDetailText = kWatchItemConfigKeyEventDetailText;
|
||||
NSString *const kWatchItemConfigKeyProcesses = @"Processes";
|
||||
NSString *const kWatchItemConfigKeyProcessesBinaryPath = @"BinaryPath";
|
||||
NSString *const kWatchItemConfigKeyProcessesCertificateSha256 = @"CertificateSha256";
|
||||
@@ -80,6 +84,18 @@ static constexpr uint64_t kMinReapplyConfigFrequencySecs = 15;
|
||||
// potential unbounded lengths, but no real reason this cannot be higher.
|
||||
static constexpr NSUInteger kWatchItemConfigOptionCustomMessageMaxLength = 2048;
|
||||
|
||||
// Semi-arbitrary max event detail text length. The text has to fit on a button
|
||||
// and shouldn't be too large.
|
||||
static constexpr NSUInteger kWatchItemConfigEventDetailTextMaxLength = 48;
|
||||
|
||||
// Servers are recommended to support up to 8000 octets.
|
||||
// https://www.rfc-editor.org/rfc/rfc9110#section-4.1-5
|
||||
//
|
||||
// Seems excessive but no good reason to not allow long URLs. However because
|
||||
// the URL supports pseudo-format strings that can extend the length, a smaller
|
||||
// max is used here.
|
||||
static constexpr NSUInteger kWatchItemConfigEventDetailURLMaxLength = 6000;
|
||||
|
||||
namespace santa::santad::data_layer {
|
||||
|
||||
// Type aliases
|
||||
@@ -151,8 +167,8 @@ ValidatorBlock HexValidator(NSUInteger expected_length) {
|
||||
};
|
||||
}
|
||||
|
||||
// Given a max length, returns a ValidatorBlock that confirms the
|
||||
// string is a not longer than the max.
|
||||
// Given a min and max length, returns a ValidatorBlock that confirms the
|
||||
// string is within the given bounds.
|
||||
ValidatorBlock LenRangeValidator(NSUInteger min_length, NSUInteger max_length) {
|
||||
return ^bool(NSString *val, NSError **err) {
|
||||
if (val.length < min_length) {
|
||||
@@ -396,6 +412,10 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
|
||||
/// <true/>
|
||||
/// <key>BlockMessage</key>
|
||||
/// <string>...</string>
|
||||
/// <key>EventDetailURL</key>
|
||||
/// <string>...</string>
|
||||
/// <key>EventDetailText</key>
|
||||
/// <string>...</string>
|
||||
/// </dict>
|
||||
/// <key>Processes</key>
|
||||
/// <array>
|
||||
@@ -441,6 +461,16 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
LenRangeValidator(0, kWatchItemConfigOptionCustomMessageMaxLength))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsEventDetailURL, [NSString class], err,
|
||||
false, LenRangeValidator(0, kWatchItemConfigEventDetailURLMaxLength))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsEventDetailText, [NSString class], err,
|
||||
false, LenRangeValidator(0, kWatchItemConfigEventDetailTextMaxLength))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool allow_read_access = GetBoolValue(options, kWatchItemConfigKeyOptionsAllowReadAccess,
|
||||
@@ -466,7 +496,8 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
allow_read_access, audit_only, invert_process_exceptions, enable_silent_mode,
|
||||
enable_silent_tty_mode,
|
||||
NSStringToUTF8StringView(options[kWatchItemConfigKeyOptionsCustomMessage]),
|
||||
std::get<ProcessList>(proc_list)));
|
||||
options[kWatchItemConfigKeyOptionsEventDetailURL],
|
||||
options[kWatchItemConfigKeyOptionsEventDetailText], std::get<ProcessList>(proc_list)));
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -514,6 +545,16 @@ bool ParseConfig(NSDictionary *config, std::vector<std::shared_ptr<WatchItemPoli
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VerifyConfigKey(config, kWatchItemConfigKeyEventDetailURL, [NSString class], err, false,
|
||||
LenRangeValidator(0, kWatchItemConfigEventDetailURLMaxLength))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VerifyConfigKey(config, kWatchItemConfigKeyEventDetailText, [NSString class], err, false,
|
||||
LenRangeValidator(0, kWatchItemConfigEventDetailTextMaxLength))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config[kWatchItemConfigKeyWatchItems] &&
|
||||
![config[kWatchItemConfigKeyWatchItems] isKindOfClass:[NSDictionary class]]) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Top level key '%@' must be a dictionary",
|
||||
@@ -688,8 +729,18 @@ void WatchItems::UpdateCurrentState(
|
||||
current_config_ = new_config;
|
||||
if (new_config) {
|
||||
policy_version_ = NSStringToUTF8String(new_config[kWatchItemConfigKeyVersion]);
|
||||
// Non-existent kWatchItemConfigKeyEventDetailURL key or zero length value
|
||||
// will both result in a nil global policy event detail URL.
|
||||
if (((NSString *)new_config[kWatchItemConfigKeyEventDetailURL]).length) {
|
||||
policy_event_detail_url_ = new_config[kWatchItemConfigKeyEventDetailURL];
|
||||
} else {
|
||||
policy_event_detail_url_ = nil;
|
||||
}
|
||||
policy_event_detail_text_ = new_config[kWatchItemConfigKeyEventDetailText];
|
||||
} else {
|
||||
policy_version_ = "";
|
||||
policy_event_detail_url_ = nil;
|
||||
policy_event_detail_text_ = nil;
|
||||
}
|
||||
|
||||
last_update_time_ = [[NSDate date] timeIntervalSince1970];
|
||||
@@ -821,4 +872,29 @@ std::optional<WatchItemsState> WatchItems::State() {
|
||||
return state;
|
||||
}
|
||||
|
||||
std::pair<NSString *, NSString *> WatchItems::EventDetailLinkInfo(
|
||||
const std::shared_ptr<WatchItemPolicy> &watch_item) {
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
if (!watch_item) {
|
||||
return {policy_event_detail_url_, policy_event_detail_text_};
|
||||
}
|
||||
|
||||
NSString *url = watch_item->event_detail_url.has_value() ? watch_item->event_detail_url.value()
|
||||
: policy_event_detail_url_;
|
||||
|
||||
NSString *text = watch_item->event_detail_text.has_value() ? watch_item->event_detail_text.value()
|
||||
: policy_event_detail_text_;
|
||||
|
||||
// Ensure empty strings are repplaced with nil
|
||||
if (!url.length) {
|
||||
url = nil;
|
||||
}
|
||||
|
||||
if (!text.length) {
|
||||
text = nil;
|
||||
}
|
||||
|
||||
return {url, text};
|
||||
}
|
||||
|
||||
} // namespace santa::santad::data_layer
|
||||
|
||||
@@ -831,7 +831,7 @@ static NSMutableDictionary *WrapWatchItemsConfig(NSDictionary *config) {
|
||||
*policies[0].get(),
|
||||
WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType,
|
||||
kWatchItemPolicyDefaultAllowReadAccess, kWatchItemPolicyDefaultAuditOnly,
|
||||
kWatchItemPolicyDefaultInvertProcessExceptions, {}));
|
||||
kWatchItemPolicyDefaultInvertProcessExceptions));
|
||||
|
||||
// Test multiple paths, options, and processes
|
||||
policies.clear();
|
||||
@@ -859,10 +859,12 @@ static NSMutableDictionary *WrapWatchItemsConfig(NSDictionary *config) {
|
||||
policies, &err));
|
||||
|
||||
XCTAssertEqual(policies.size(), 2);
|
||||
XCTAssertEqual(*policies[0].get(), WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType,
|
||||
true, false, true, true, false, "", procs));
|
||||
XCTAssertEqual(*policies[1].get(), WatchItemPolicy("rule", "b", WatchItemPathType::kPrefix, true,
|
||||
false, true, true, false, "", procs));
|
||||
XCTAssertEqual(*policies[0].get(),
|
||||
WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType, true, false, true,
|
||||
true, false, "", nil, nil, procs));
|
||||
XCTAssertEqual(*policies[1].get(),
|
||||
WatchItemPolicy("rule", "b", WatchItemPathType::kPrefix, true, false, true, true,
|
||||
false, "", nil, nil, procs));
|
||||
}
|
||||
|
||||
- (void)testState {
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <DiskArbitration/DiskArbitration.h>
|
||||
#include <Foundation/Foundation.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/ucred.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -27,6 +30,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@interface MockDADisk : NSObject
|
||||
@property(nonatomic) NSDictionary *diskDescription;
|
||||
@property(nonatomic, readwrite) NSString *name;
|
||||
@property(nonatomic) BOOL wasMounted;
|
||||
@property(nonatomic) BOOL wasUnmounted;
|
||||
@end
|
||||
|
||||
typedef void (^MockDADiskAppearedCallback)(DADiskRef ref);
|
||||
@@ -36,19 +41,35 @@ typedef void (^MockDADiskAppearedCallback)(DADiskRef ref);
|
||||
NSMutableDictionary<NSString *, MockDADisk *> *insertedDevices;
|
||||
@property(nonatomic, readwrite, nonnull)
|
||||
NSMutableArray<MockDADiskAppearedCallback> *diskAppearedCallbacks;
|
||||
@property(nonatomic) BOOL wasRemounted;
|
||||
@property(nonatomic, nullable) dispatch_queue_t sessionQueue;
|
||||
|
||||
- (instancetype _Nonnull)init;
|
||||
- (void)reset;
|
||||
|
||||
// Also triggers DADiskRegisterDiskAppearedCallback
|
||||
- (void)insert:(MockDADisk *)ref bsdName:(NSString *)bsdName;
|
||||
- (void)insert:(MockDADisk *)ref;
|
||||
|
||||
// Retrieve an initialized singleton MockDiskArbitration object
|
||||
+ (instancetype _Nonnull)mockDiskArbitration;
|
||||
@end
|
||||
|
||||
@interface MockStatfs : NSObject
|
||||
@property NSString *fromName;
|
||||
@property NSString *onName;
|
||||
@property NSNumber *flags;
|
||||
|
||||
- (instancetype _Nonnull)initFrom:(NSString *)from on:(NSString *)on flags:(NSNumber *)flags;
|
||||
@end
|
||||
|
||||
@interface MockMounts : NSObject
|
||||
@property(nonatomic) NSMutableDictionary<NSString *, MockStatfs *> *mounts;
|
||||
|
||||
- (instancetype _Nonnull)init;
|
||||
- (void)reset;
|
||||
- (void)insert:(MockStatfs *)sfs;
|
||||
+ (instancetype _Nonnull)mockMounts;
|
||||
@end
|
||||
|
||||
//
|
||||
// All DiskArbitration functions used in SNTEndpointSecurityDeviceManager
|
||||
// and shimmed out accordingly.
|
||||
@@ -81,5 +102,9 @@ void DARegisterDiskDescriptionChangedCallback(DASessionRef session,
|
||||
void DASessionSetDispatchQueue(DASessionRef session, dispatch_queue_t __nullable queue);
|
||||
DASessionRef __nullable DASessionCreate(CFAllocatorRef __nullable allocator);
|
||||
|
||||
void DADiskUnmount(DADiskRef disk, DADiskUnmountOptions options,
|
||||
DADiskUnmountCallback __nullable callback, void *__nullable context);
|
||||
int getmntinfo_r_np(struct statfs *__nullable *__nullable mntbufp, int flags);
|
||||
|
||||
CF_EXTERN_C_END
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/ucred.h>
|
||||
|
||||
#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h"
|
||||
|
||||
@@ -37,11 +40,14 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
[self.insertedDevices removeAllObjects];
|
||||
[self.diskAppearedCallbacks removeAllObjects];
|
||||
self.sessionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
self.wasRemounted = NO;
|
||||
}
|
||||
|
||||
- (void)insert:(MockDADisk *)ref bsdName:(NSString *)bsdName {
|
||||
self.insertedDevices[bsdName] = ref;
|
||||
- (void)insert:(MockDADisk *)ref {
|
||||
if (!ref.diskDescription[@"DAMediaBSDName"]) {
|
||||
[NSException raise:@"Missing DAMediaBSDName"
|
||||
format:@"The MockDADisk is missing the DAMediaBSDName diskDescription key."];
|
||||
}
|
||||
self.insertedDevices[ref.diskDescription[@"DAMediaBSDName"]] = ref;
|
||||
|
||||
for (MockDADiskAppearedCallback callback in self.diskAppearedCallbacks) {
|
||||
dispatch_sync(self.sessionQueue, ^{
|
||||
@@ -62,12 +68,58 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@end
|
||||
|
||||
@implementation MockStatfs
|
||||
- (instancetype _Nonnull)initFrom:(NSString *)from on:(NSString *)on flags:(NSNumber *)flags {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_fromName = from;
|
||||
_onName = on;
|
||||
_flags = flags;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation MockMounts
|
||||
|
||||
- (instancetype _Nonnull)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_mounts = [NSMutableDictionary dictionary];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)reset {
|
||||
[self.mounts removeAllObjects];
|
||||
}
|
||||
|
||||
- (void)insert:(MockStatfs *)sfs {
|
||||
self.mounts[sfs.fromName] = sfs;
|
||||
}
|
||||
|
||||
+ (instancetype _Nonnull)mockMounts {
|
||||
static MockMounts *sharedMounts;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedMounts = [[MockMounts alloc] init];
|
||||
});
|
||||
return sharedMounts;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
void DADiskMountWithArguments(DADiskRef _Nonnull disk, CFURLRef __nullable path,
|
||||
DADiskMountOptions options, DADiskMountCallback __nullable callback,
|
||||
void *__nullable context,
|
||||
CFStringRef __nullable arguments[_Nullable]) {
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
mockDA.wasRemounted = YES;
|
||||
MockDADisk *mockDisk = (__bridge MockDADisk *)disk;
|
||||
mockDisk.wasMounted = YES;
|
||||
|
||||
if (context) {
|
||||
dispatch_semaphore_t sema = (__bridge dispatch_semaphore_t)context;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}
|
||||
}
|
||||
|
||||
DADiskRef __nullable DADiskCreateFromBSDName(CFAllocatorRef __nullable allocator,
|
||||
@@ -117,4 +169,32 @@ DASessionRef __nullable DASessionCreate(CFAllocatorRef __nullable allocator) {
|
||||
return (__bridge DASessionRef)[MockDiskArbitration mockDiskArbitration];
|
||||
};
|
||||
|
||||
void DADiskUnmount(DADiskRef disk, DADiskUnmountOptions options,
|
||||
DADiskUnmountCallback __nullable callback, void *__nullable context) {
|
||||
MockDADisk *mockDisk = (__bridge MockDADisk *)disk;
|
||||
mockDisk.wasUnmounted = YES;
|
||||
|
||||
dispatch_semaphore_t sema = (__bridge dispatch_semaphore_t)context;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}
|
||||
|
||||
int getmntinfo_r_np(struct statfs *__nullable *__nullable mntbufp, int flags) {
|
||||
MockMounts *mockMounts = [MockMounts mockMounts];
|
||||
|
||||
struct statfs *sfs = (struct statfs *)calloc(mockMounts.mounts.count, sizeof(struct statfs));
|
||||
|
||||
__block NSUInteger i = 0;
|
||||
[mockMounts.mounts
|
||||
enumerateKeysAndObjectsUsingBlock:^(NSString *key, MockStatfs *mockSfs, BOOL *stop) {
|
||||
strlcpy(sfs[i].f_mntfromname, mockSfs.fromName.UTF8String, sizeof(sfs[i].f_mntfromname));
|
||||
strlcpy(sfs[i].f_mntonname, mockSfs.onName.UTF8String, sizeof(sfs[i].f_mntonname));
|
||||
sfs[i].f_flags = [mockSfs.flags unsignedIntValue];
|
||||
i++;
|
||||
}];
|
||||
|
||||
*mntbufp = sfs;
|
||||
|
||||
return (int)mockMounts.mounts.count;
|
||||
}
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -320,9 +320,19 @@ class EnrichedUnlink : public EnrichedEventType {
|
||||
EnrichedFile target_;
|
||||
};
|
||||
|
||||
class EnrichedCSInvalidated : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedCSInvalidated(Message &&es_msg, EnrichedProcess &&instigator)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)) {}
|
||||
EnrichedCSInvalidated(EnrichedCSInvalidated &&other)
|
||||
: EnrichedEventType(std::move(other)) {}
|
||||
EnrichedCSInvalidated(const EnrichedCSInvalidated &other) = delete;
|
||||
};
|
||||
|
||||
using EnrichedType =
|
||||
std::variant<EnrichedClose, EnrichedExchange, EnrichedExec, EnrichedExit,
|
||||
EnrichedFork, EnrichedLink, EnrichedRename, EnrichedUnlink>;
|
||||
EnrichedFork, EnrichedLink, EnrichedRename, EnrichedUnlink,
|
||||
EnrichedCSInvalidated>;
|
||||
|
||||
class EnrichedMessage {
|
||||
public:
|
||||
|
||||
@@ -74,6 +74,9 @@ std::unique_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK:
|
||||
return std::make_unique<EnrichedMessage>(EnrichedUnlink(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.unlink.target)));
|
||||
case ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED:
|
||||
return std::make_unique<EnrichedMessage>(
|
||||
EnrichedCSInvalidated(std::move(es_msg), Enrich(*es_msg->process)));
|
||||
default:
|
||||
// This is a programming error
|
||||
LOGE(@"Attempting to enrich an unhandled event type: %d", es_msg->event_type);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <DiskArbitration/DiskArbitration.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#import "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
@@ -39,11 +40,16 @@ typedef void (^SNTDeviceBlockCallback)(SNTDeviceEvent *event);
|
||||
@property(nonatomic, nullable) SNTDeviceBlockCallback deviceBlockCallback;
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
|
||||
authResultCache:(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache;
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)
|
||||
esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
|
||||
authResultCache:
|
||||
(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache
|
||||
blockUSBMount:(BOOL)blockUSBMount
|
||||
remountUSBMode:(nullable NSArray<NSString *> *)remountUSBMode
|
||||
startupPreferences:(SNTDeviceManagerStartupPreferences)startupPrefs;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -24,9 +24,12 @@
|
||||
#include <errno.h>
|
||||
#include <libproc.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/ucred.h>
|
||||
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
@@ -38,32 +41,53 @@ using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
|
||||
// Defined operations for startup metrics:
|
||||
// Device shouldn't be operated on (e.g. not a mass storage device)
|
||||
static NSString *const kMetricStartupDiskOperationSkip = @"Skipped";
|
||||
// Device already had appropriate flags set
|
||||
static NSString *const kMetricStartupDiskOperationAllowed = @"Allowed";
|
||||
// Device failed to be unmounted
|
||||
static NSString *const kMetricStartupDiskOperationUnmountFailed = @"UnmountFailed";
|
||||
// Device failed to be remounted
|
||||
static NSString *const kMetricStartupDiskOperationRemountFailed = @"RemountFailed";
|
||||
// Remounts were requested, but remount args weren't set
|
||||
static NSString *const kMetricStartupDiskOperationRemountSkipped = @"RemountSkipped";
|
||||
// Operations on device matching the configured startup pref wwere successful
|
||||
static NSString *const kMetricStartupDiskOperationSuccess = @"Success";
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManager ()
|
||||
|
||||
- (void)logDiskAppeared:(NSDictionary *)props;
|
||||
- (void)logDiskDisappeared:(NSDictionary *)props;
|
||||
|
||||
@property SNTMetricCounter *startupDiskMetrics;
|
||||
@property DASessionRef diskArbSession;
|
||||
@property(nonatomic, readonly) dispatch_queue_t diskQueue;
|
||||
@property dispatch_semaphore_t diskSema;
|
||||
|
||||
@end
|
||||
|
||||
void diskMountedCallback(DADiskRef disk, DADissenterRef dissenter, void *context) {
|
||||
void DiskMountedCallback(DADiskRef disk, DADissenterRef dissenter, void *context) {
|
||||
if (dissenter) {
|
||||
DAReturn status = DADissenterGetStatus(dissenter);
|
||||
|
||||
NSString *statusString = (NSString *)DADissenterGetStatusString(dissenter);
|
||||
IOReturn systemCode = err_get_system(status);
|
||||
IOReturn subSystemCode = err_get_sub(status);
|
||||
IOReturn errorCode = err_get_code(status);
|
||||
|
||||
LOGE(@"SNTEndpointSecurityDeviceManager: dissenter status codes: system: %d, subsystem: %d, "
|
||||
@"err: %d; status: %s",
|
||||
systemCode, subSystemCode, errorCode, [statusString UTF8String]);
|
||||
@"err: %d; status: %@",
|
||||
systemCode, subSystemCode, errorCode,
|
||||
CFBridgingRelease(DADissenterGetStatusString(dissenter)));
|
||||
}
|
||||
|
||||
if (context) {
|
||||
dispatch_semaphore_t sema = (__bridge dispatch_semaphore_t)context;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}
|
||||
}
|
||||
|
||||
void diskAppearedCallback(DADiskRef disk, void *context) {
|
||||
void DiskAppearedCallback(DADiskRef disk, void *context) {
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
SNTEndpointSecurityDeviceManager *dm = (__bridge SNTEndpointSecurityDeviceManager *)context;
|
||||
@@ -71,7 +95,7 @@ void diskAppearedCallback(DADiskRef disk, void *context) {
|
||||
[dm logDiskAppeared:props];
|
||||
}
|
||||
|
||||
void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *context) {
|
||||
void DiskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *context) {
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
|
||||
@@ -82,7 +106,7 @@ void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *conte
|
||||
}
|
||||
}
|
||||
|
||||
void diskDisappearedCallback(DADiskRef disk, void *context) {
|
||||
void DiskDisappearedCallback(DADiskRef disk, void *context) {
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
|
||||
@@ -91,7 +115,22 @@ void diskDisappearedCallback(DADiskRef disk, void *context) {
|
||||
[dm logDiskDisappeared:props];
|
||||
}
|
||||
|
||||
NSArray<NSString *> *maskToMountArgs(long remountOpts) {
|
||||
void DiskUnmountCallback(DADiskRef disk, DADissenterRef dissenter, void *context) {
|
||||
if (dissenter) {
|
||||
LOGW(@"Unable to unmount device: %@", CFBridgingRelease(DADissenterGetStatusString(dissenter)));
|
||||
} else if (disk) {
|
||||
NSDictionary *diskInfo = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
LOGI(@"Unmounted device: Model: %@, Vendor: %@, Path: %@",
|
||||
diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceModelKey],
|
||||
diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceVendorKey],
|
||||
diskInfo[(__bridge NSString *)kDADiskDescriptionVolumePathKey]);
|
||||
}
|
||||
|
||||
dispatch_semaphore_t sema = (__bridge dispatch_semaphore_t)context;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}
|
||||
|
||||
NSArray<NSString *> *maskToMountArgs(uint32_t remountOpts) {
|
||||
NSMutableArray<NSString *> *args = [NSMutableArray array];
|
||||
if (remountOpts & MNT_RDONLY) [args addObject:@"rdonly"];
|
||||
if (remountOpts & MNT_NOEXEC) [args addObject:@"noexec"];
|
||||
@@ -104,28 +143,29 @@ NSArray<NSString *> *maskToMountArgs(long remountOpts) {
|
||||
return args;
|
||||
}
|
||||
|
||||
long mountArgsToMask(NSArray<NSString *> *args) {
|
||||
long flags = 0;
|
||||
uint32_t mountArgsToMask(NSArray<NSString *> *args) {
|
||||
uint32_t flags = 0;
|
||||
for (NSString *i in args) {
|
||||
NSString *arg = [i lowercaseString];
|
||||
if ([arg isEqualToString:@"rdonly"])
|
||||
if ([arg isEqualToString:@"rdonly"]) {
|
||||
flags |= MNT_RDONLY;
|
||||
else if ([arg isEqualToString:@"noexec"])
|
||||
} else if ([arg isEqualToString:@"noexec"]) {
|
||||
flags |= MNT_NOEXEC;
|
||||
else if ([arg isEqualToString:@"nosuid"])
|
||||
} else if ([arg isEqualToString:@"nosuid"]) {
|
||||
flags |= MNT_NOSUID;
|
||||
else if ([arg isEqualToString:@"nobrowse"])
|
||||
} else if ([arg isEqualToString:@"nobrowse"]) {
|
||||
flags |= MNT_DONTBROWSE;
|
||||
else if ([arg isEqualToString:@"noowners"])
|
||||
} else if ([arg isEqualToString:@"noowners"]) {
|
||||
flags |= MNT_UNKNOWNPERMISSIONS;
|
||||
else if ([arg isEqualToString:@"nodev"])
|
||||
} else if ([arg isEqualToString:@"nodev"]) {
|
||||
flags |= MNT_NODEV;
|
||||
else if ([arg isEqualToString:@"-j"])
|
||||
} else if ([arg isEqualToString:@"-j"]) {
|
||||
flags |= MNT_JOURNALED;
|
||||
else if ([arg isEqualToString:@"async"])
|
||||
} else if ([arg isEqualToString:@"async"]) {
|
||||
flags |= MNT_ASYNC;
|
||||
else
|
||||
} else {
|
||||
LOGE(@"SNTEndpointSecurityDeviceManager: unexpected mount arg: %@", arg);
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
@@ -140,25 +180,205 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
logger:(std::shared_ptr<Logger>)logger
|
||||
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache {
|
||||
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache
|
||||
blockUSBMount:(BOOL)blockUSBMount
|
||||
remountUSBMode:(nullable NSArray<NSString *> *)remountUSBMode
|
||||
startupPreferences:(SNTDeviceManagerStartupPreferences)startupPrefs {
|
||||
self = [super initWithESAPI:std::move(esApi)
|
||||
metrics:std::move(metrics)
|
||||
processor:santa::santad::Processor::kDeviceManager];
|
||||
if (self) {
|
||||
_logger = logger;
|
||||
_authResultCache = authResultCache;
|
||||
_blockUSBMount = false;
|
||||
_blockUSBMount = blockUSBMount;
|
||||
_remountArgs = remountUSBMode;
|
||||
|
||||
_diskQueue = dispatch_queue_create("com.google.santa.daemon.disk_queue", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
_diskArbSession = DASessionCreate(NULL);
|
||||
DASessionSetDispatchQueue(_diskArbSession, _diskQueue);
|
||||
|
||||
SNTMetricInt64Gauge *startupPrefsMetric =
|
||||
[[SNTMetricSet sharedInstance] int64GaugeWithName:@"/santa/device_manager/startup_preference"
|
||||
fieldNames:@[]
|
||||
helpText:@"The current startup preference value"];
|
||||
|
||||
[[SNTMetricSet sharedInstance] registerCallback:^{
|
||||
[startupPrefsMetric set:startupPrefs forFieldValues:@[]];
|
||||
}];
|
||||
|
||||
_startupDiskMetrics = [[SNTMetricSet sharedInstance]
|
||||
counterWithName:@"/santa/device_manager/startup_disk_operation"
|
||||
fieldNames:@[ @"operation" ]
|
||||
helpText:@"Count of the number of USB devices encountered per operation"];
|
||||
|
||||
[self performStartupTasks:startupPrefs];
|
||||
|
||||
[self establishClientOrDie];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (uint32_t)updatedMountFlags:(struct statfs *)sfs {
|
||||
uint32_t mask = sfs->f_flags | mountArgsToMask(self.remountArgs);
|
||||
|
||||
// NB: APFS mounts get MNT_JOURNALED implicitly set. However, mount_apfs
|
||||
// does not support the `-j` option so this flag needs to be cleared.
|
||||
if (strncmp(sfs->f_fstypename, "apfs", sizeof(sfs->f_fstypename)) == 0) {
|
||||
mask &= ~MNT_JOURNALED;
|
||||
}
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
- (BOOL)shouldOperateOnDisk:(DADiskRef)disk {
|
||||
NSDictionary *diskInfo = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
|
||||
BOOL isInternal = [diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceInternalKey] boolValue];
|
||||
BOOL isRemovable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaRemovableKey] boolValue];
|
||||
BOOL isEjectable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaEjectableKey] boolValue];
|
||||
NSString *protocol = diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey];
|
||||
BOOL isUSB = [protocol isEqualToString:@"USB"];
|
||||
BOOL isSecureDigital = [protocol isEqualToString:@"Secure Digital"];
|
||||
BOOL isVirtual = [protocol isEqualToString:@"Virtual Interface"];
|
||||
|
||||
NSString *kind = diskInfo[(__bridge NSString *)kDADiskDescriptionMediaKindKey];
|
||||
|
||||
// TODO: check kind and protocol for banned things (e.g. MTP).
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d "
|
||||
@"isRemovable: %d isEjectable: %d",
|
||||
protocol, kind, isInternal, isRemovable, isEjectable);
|
||||
|
||||
// if the device is internal, or virtual *AND* is not an SD Card,
|
||||
// then allow the mount. This is to ensure we block SD cards inserted into
|
||||
// the internal reader of some Macs, whilst also ensuring we don't block
|
||||
// the internal storage device.
|
||||
if ((isInternal || isVirtual) && !isSecureDigital) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We are okay with operations for devices that are non-removable as long as
|
||||
// they are NOT a USB device, or an SD Card.
|
||||
if (!isRemovable && !isEjectable && !isUSB && !isSecureDigital) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
- (BOOL)haveRemountArgs {
|
||||
return [self.remountArgs count] > 0;
|
||||
}
|
||||
|
||||
- (BOOL)remountUSBModeContainsFlags:(uint32_t)flags {
|
||||
if (![self haveRemountArgs]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t requiredFlags = mountArgsToMask(self.remountArgs);
|
||||
|
||||
LOGD(@" Got mount flags: 0x%08x | %@", flags, maskToMountArgs(flags));
|
||||
LOGD(@"Want mount flags: 0x%08x | %@", mountArgsToMask(self.remountArgs), self.remountArgs);
|
||||
|
||||
return (flags & requiredFlags) == requiredFlags;
|
||||
}
|
||||
|
||||
- (void)incrementStartupMetricsOperation:(NSString *)op {
|
||||
[self.startupDiskMetrics incrementForFieldValues:@[ op ]];
|
||||
}
|
||||
|
||||
// NB: Remount options are implemented as separate "unmount" and "mount"
|
||||
// operations instead of using the "update"/MNT_UPDATE flag. This is because
|
||||
// filesystems often don't support many transitions (e.g. RW to RO). Performing
|
||||
// the two step process has a higher chance of succeeding.
|
||||
- (void)performStartupTasks:(SNTDeviceManagerStartupPreferences)startupPrefs {
|
||||
if (!self.blockUSBMount || (startupPrefs != SNTDeviceManagerStartupPreferencesUnmount &&
|
||||
startupPrefs != SNTDeviceManagerStartupPreferencesForceUnmount &&
|
||||
startupPrefs != SNTDeviceManagerStartupPreferencesRemount &&
|
||||
startupPrefs != SNTDeviceManagerStartupPreferencesForceRemount)) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct statfs *mnts;
|
||||
int numMounts = getmntinfo_r_np(&mnts, MNT_WAIT);
|
||||
|
||||
if (numMounts == 0) {
|
||||
LOGE(@"Failed to get mount info: %d: %s", errno, strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
self.diskSema = dispatch_semaphore_create(0);
|
||||
|
||||
for (int i = 0; i < numMounts; i++) {
|
||||
struct statfs *sfs = &mnts[i];
|
||||
|
||||
DADiskRef disk = DADiskCreateFromBSDName(NULL, self.diskArbSession, sfs->f_mntfromname);
|
||||
if (!disk) {
|
||||
LOGW(@"Unable to create disk reference for device: '%s' -> '%s'", sfs->f_mntfromname,
|
||||
sfs->f_mntonname);
|
||||
continue;
|
||||
}
|
||||
|
||||
CFAutorelease(disk);
|
||||
|
||||
if (![self shouldOperateOnDisk:disk]) {
|
||||
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationSkip];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ([self remountUSBModeContainsFlags:sfs->f_flags]) {
|
||||
LOGI(@"Allowing existing mount as flags contain RemountUSBMode. '%s' -> '%s'",
|
||||
sfs->f_mntfromname, sfs->f_mntonname);
|
||||
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationAllowed];
|
||||
continue;
|
||||
}
|
||||
|
||||
DADiskUnmountOptions unmountOptions = kDADiskUnmountOptionDefault;
|
||||
if (startupPrefs == SNTDeviceManagerStartupPreferencesForceUnmount ||
|
||||
startupPrefs == SNTDeviceManagerStartupPreferencesForceRemount) {
|
||||
unmountOptions = kDADiskUnmountOptionForce;
|
||||
}
|
||||
|
||||
LOGI(@"Attempting to unmount device: '%s' mounted on '%s'", sfs->f_mntfromname,
|
||||
sfs->f_mntonname);
|
||||
|
||||
DADiskUnmount(disk, unmountOptions, DiskUnmountCallback, (__bridge void *)self.diskSema);
|
||||
|
||||
if (dispatch_semaphore_wait(self.diskSema,
|
||||
dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
|
||||
LOGW(
|
||||
@"Unmounting '%s' mounted on '%s' took longer than expected. Device may still be mounted.",
|
||||
sfs->f_mntfromname, sfs->f_mntonname);
|
||||
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationUnmountFailed];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (startupPrefs == SNTDeviceManagerStartupPreferencesRemount ||
|
||||
startupPrefs == SNTDeviceManagerStartupPreferencesForceRemount) {
|
||||
if (![self haveRemountArgs]) {
|
||||
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationRemountSkipped];
|
||||
LOGW(@"Remount requested during startup, but no remount args set. Leaving unmounted.");
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t newMode = [self updatedMountFlags:sfs];
|
||||
LOGI(@"Attempting to mount device again changing flags: 0x%08x --> 0x%08x", sfs->f_flags,
|
||||
newMode);
|
||||
|
||||
[self remount:disk mountMode:newMode semaphore:self.diskSema];
|
||||
|
||||
if (dispatch_semaphore_wait(self.diskSema,
|
||||
dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
|
||||
LOGW(@"Failed to remount device after unmounting: %s", sfs->f_mntfromname);
|
||||
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationRemountFailed];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationSuccess];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)logDiskAppeared:(NSDictionary *)props {
|
||||
self->_logger->LogDiskAppeared(props);
|
||||
}
|
||||
@@ -199,11 +419,11 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
}
|
||||
|
||||
- (void)enable {
|
||||
DARegisterDiskAppearedCallback(_diskArbSession, NULL, diskAppearedCallback,
|
||||
DARegisterDiskAppearedCallback(_diskArbSession, NULL, DiskAppearedCallback,
|
||||
(__bridge void *)self);
|
||||
DARegisterDiskDescriptionChangedCallback(_diskArbSession, NULL, NULL,
|
||||
diskDescriptionChangedCallback, (__bridge void *)self);
|
||||
DARegisterDiskDisappearedCallback(_diskArbSession, NULL, diskDisappearedCallback,
|
||||
DiskDescriptionChangedCallback, (__bridge void *)self);
|
||||
DARegisterDiskDisappearedCallback(_diskArbSession, NULL, DiskDisappearedCallback,
|
||||
(__bridge void *)self);
|
||||
|
||||
[super subscribeAndClearCache:{
|
||||
@@ -225,44 +445,15 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
long mountMode = eventStatFS->f_flags;
|
||||
pid_t pid = audit_token_to_pid(m->process->audit_token);
|
||||
LOGD(
|
||||
@"SNTEndpointSecurityDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %lu",
|
||||
m->process->executable->path.data, pid, mountMode);
|
||||
@"SNTEndpointSecurityDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %u",
|
||||
m->process->executable->path.data, pid, eventStatFS->f_flags);
|
||||
|
||||
DADiskRef disk = DADiskCreateFromBSDName(NULL, self.diskArbSession, eventStatFS->f_mntfromname);
|
||||
CFAutorelease(disk);
|
||||
|
||||
// TODO(tnek): Log all of the other attributes available in diskInfo into a structured log format.
|
||||
NSDictionary *diskInfo = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
BOOL isInternal = [diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceInternalKey] boolValue];
|
||||
BOOL isRemovable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaRemovableKey] boolValue];
|
||||
BOOL isEjectable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaEjectableKey] boolValue];
|
||||
NSString *protocol = diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey];
|
||||
BOOL isUSB = [protocol isEqualToString:@"USB"];
|
||||
BOOL isSecureDigital = [protocol isEqualToString:@"Secure Digital"];
|
||||
BOOL isVirtual = [protocol isEqualToString:@"Virtual Interface"];
|
||||
|
||||
NSString *kind = diskInfo[(__bridge NSString *)kDADiskDescriptionMediaKindKey];
|
||||
|
||||
// TODO: check kind and protocol for banned things (e.g. MTP).
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d "
|
||||
@"isRemovable: %d "
|
||||
@"isEjectable: %d",
|
||||
protocol, kind, isInternal, isRemovable, isEjectable);
|
||||
|
||||
// if the device is internal, or virtual *AND* is not an SD Card,
|
||||
// then allow the mount. This is to ensure we block SD cards inserted into
|
||||
// the internal reader of some Macs, whilst also ensuring we don't block
|
||||
// the internal storage device.
|
||||
if ((isInternal || isVirtual) && !isSecureDigital) {
|
||||
return ES_AUTH_RESULT_ALLOW;
|
||||
}
|
||||
|
||||
// We are okay with operations for devices that are non-removable as long as
|
||||
// they are NOT a USB device, or an SD Card.
|
||||
if (!isRemovable && !isEjectable && !isUSB && !isSecureDigital) {
|
||||
if (![self shouldOperateOnDisk:disk]) {
|
||||
return ES_AUTH_RESULT_ALLOW;
|
||||
}
|
||||
|
||||
@@ -270,24 +461,20 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
initWithOnName:[NSString stringWithUTF8String:eventStatFS->f_mntonname]
|
||||
fromName:[NSString stringWithUTF8String:eventStatFS->f_mntfromname]];
|
||||
|
||||
BOOL shouldRemount = self.remountArgs != nil && [self.remountArgs count] > 0;
|
||||
|
||||
if (shouldRemount) {
|
||||
if ([self haveRemountArgs]) {
|
||||
event.remountArgs = self.remountArgs;
|
||||
long remountOpts = mountArgsToMask(self.remountArgs);
|
||||
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: mountMode: %@", maskToMountArgs(mountMode));
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: remountOpts: %@", maskToMountArgs(remountOpts));
|
||||
|
||||
if ((mountMode & remountOpts) == remountOpts && m->event_type != ES_EVENT_TYPE_AUTH_REMOUNT) {
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: Allowing as mount as flags match remountOpts");
|
||||
if ([self remountUSBModeContainsFlags:eventStatFS->f_flags] &&
|
||||
m->event_type != ES_EVENT_TYPE_AUTH_REMOUNT) {
|
||||
LOGD(@"Allowing mount as flags contain RemountUSBMode. '%s' -> '%s'",
|
||||
eventStatFS->f_mntfromname, eventStatFS->f_mntonname);
|
||||
return ES_AUTH_RESULT_ALLOW;
|
||||
}
|
||||
|
||||
long newMode = mountMode | remountOpts;
|
||||
LOGI(@"SNTEndpointSecurityDeviceManager: remounting device '%s'->'%s', flags (%lu) -> (%lu)",
|
||||
eventStatFS->f_mntfromname, eventStatFS->f_mntonname, mountMode, newMode);
|
||||
[self remount:disk mountMode:newMode];
|
||||
uint32_t newMode = [self updatedMountFlags:eventStatFS];
|
||||
LOGI(@"SNTEndpointSecurityDeviceManager: remounting device '%s'->'%s', flags (%u) -> (%u)",
|
||||
eventStatFS->f_mntfromname, eventStatFS->f_mntonname, eventStatFS->f_flags, newMode);
|
||||
[self remount:disk mountMode:newMode semaphore:nil];
|
||||
}
|
||||
|
||||
if (self.deviceBlockCallback) {
|
||||
@@ -297,14 +484,16 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return ES_AUTH_RESULT_DENY;
|
||||
}
|
||||
|
||||
- (void)remount:(DADiskRef)disk mountMode:(long)remountMask {
|
||||
- (void)remount:(DADiskRef)disk
|
||||
mountMode:(uint32_t)remountMask
|
||||
semaphore:(nullable dispatch_semaphore_t)sema {
|
||||
NSArray<NSString *> *args = maskToMountArgs(remountMask);
|
||||
CFStringRef *argv = (CFStringRef *)calloc(args.count + 1, sizeof(CFStringRef));
|
||||
CFArrayGetValues((__bridge CFArrayRef)args, CFRangeMake(0, (CFIndex)args.count),
|
||||
(const void **)argv);
|
||||
|
||||
DADiskMountWithArguments(disk, NULL, kDADiskMountOptionDefault, diskMountedCallback,
|
||||
(__bridge void *)self, (CFStringRef *)argv);
|
||||
DADiskMountWithArguments(disk, NULL, kDADiskMountOptionDefault, DiskMountedCallback,
|
||||
(__bridge void *)sema, (CFStringRef *)argv);
|
||||
|
||||
free(argv);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
@@ -50,12 +51,17 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManager (Testing)
|
||||
- (instancetype)init;
|
||||
- (void)logDiskAppeared:(NSDictionary *)props;
|
||||
- (BOOL)shouldOperateOnDisk:(DADiskRef)disk;
|
||||
- (void)performStartupTasks:(SNTDeviceManagerStartupPreferences)startupPrefs;
|
||||
- (uint32_t)updatedMountFlags:(struct statfs *)sfs;
|
||||
@end
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManagerTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@property MockDiskArbitration *mockDA;
|
||||
@property MockMounts *mockMounts;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityDeviceManagerTest
|
||||
@@ -70,6 +76,9 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
self.mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[self.mockDA reset];
|
||||
|
||||
self.mockMounts = [MockMounts mockMounts];
|
||||
[self.mockMounts reset];
|
||||
|
||||
fclose(stdout);
|
||||
}
|
||||
|
||||
@@ -112,7 +121,10 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
logger:nullptr
|
||||
authResultCache:nullptr];
|
||||
authResultCache:nullptr
|
||||
blockUSBMount:false
|
||||
remountUSBMode:nil
|
||||
startupPreferences:SNTDeviceManagerStartupPreferencesNone];
|
||||
|
||||
setupDMCallback(deviceManager);
|
||||
|
||||
@@ -120,7 +132,7 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
id partialDeviceManager = OCMPartialMock(deviceManager);
|
||||
OCMStub([partialDeviceManager logDiskAppeared:OCMOCK_ANY]);
|
||||
|
||||
[self.mockDA insert:disk bsdName:test_mntfromname];
|
||||
[self.mockDA insert:disk];
|
||||
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
@@ -211,7 +223,8 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
};
|
||||
}];
|
||||
|
||||
XCTAssertEqual(self.mockDA.wasRemounted, YES);
|
||||
XCTAssertEqual(self.mockDA.insertedDevices.count, 1);
|
||||
XCTAssertTrue([self.mockDA.insertedDevices allValues][0].wasMounted);
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
@@ -274,7 +287,8 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
};
|
||||
}];
|
||||
|
||||
XCTAssertEqual(self.mockDA.wasRemounted, YES);
|
||||
XCTAssertEqual(self.mockDA.insertedDevices.count, 1);
|
||||
XCTAssertTrue([self.mockDA.insertedDevices allValues][0].wasMounted);
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:10.0];
|
||||
|
||||
@@ -303,7 +317,8 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
};
|
||||
}];
|
||||
|
||||
XCTAssertEqual(self.mockDA.wasRemounted, NO);
|
||||
XCTAssertEqual(self.mockDA.insertedDevices.count, 1);
|
||||
XCTAssertFalse([self.mockDA.insertedDevices allValues][0].wasMounted);
|
||||
}
|
||||
|
||||
- (void)testNotifyUnmountFlushesCache {
|
||||
@@ -324,7 +339,10 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
logger:nullptr
|
||||
authResultCache:mockAuthCache];
|
||||
authResultCache:mockAuthCache
|
||||
blockUSBMount:YES
|
||||
remountUSBMode:nil
|
||||
startupPreferences:SNTDeviceManagerStartupPreferencesNone];
|
||||
|
||||
deviceManager.blockUSBMount = YES;
|
||||
|
||||
@@ -340,6 +358,122 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get());
|
||||
}
|
||||
|
||||
- (void)testPerformStartupTasks {
|
||||
SNTEndpointSecurityDeviceManager *deviceManager = [[SNTEndpointSecurityDeviceManager alloc] init];
|
||||
|
||||
id partialDeviceManager = OCMPartialMock(deviceManager);
|
||||
OCMStub([partialDeviceManager shouldOperateOnDisk:nil]).ignoringNonObjectArgs().andReturn(YES);
|
||||
|
||||
deviceManager.blockUSBMount = YES;
|
||||
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];
|
||||
|
||||
[self.mockMounts insert:[[MockStatfs alloc] initFrom:@"d1" on:@"v1" flags:@(0x0)]];
|
||||
[self.mockMounts insert:[[MockStatfs alloc] initFrom:@"d2"
|
||||
on:@"v2"
|
||||
flags:@(MNT_RDONLY | MNT_NOEXEC | MNT_JOURNALED)]];
|
||||
|
||||
// Create mock disks with desired args
|
||||
MockDADisk * (^CreateMockDisk)(NSString *, NSString *) =
|
||||
^MockDADisk *(NSString *mountOn, NSString *mountFrom) {
|
||||
MockDADisk *mockDisk = [[MockDADisk alloc] init];
|
||||
mockDisk.diskDescription = @{
|
||||
@"DAVolumePath" : mountOn, // f_mntonname,
|
||||
@"DADevicePath" : mountOn, // f_mntonname,
|
||||
@"DAMediaBSDName" : mountFrom, // f_mntfromname,
|
||||
};
|
||||
|
||||
return mockDisk;
|
||||
};
|
||||
|
||||
// Reset the Mock DA property, setup disks and remount args, then trigger the test
|
||||
void (^PerformStartupTest)(NSArray<MockDADisk *> *, NSArray<NSString *> *,
|
||||
SNTDeviceManagerStartupPreferences) =
|
||||
^void(NSArray<MockDADisk *> *disks, NSArray<NSString *> *remountArgs,
|
||||
SNTDeviceManagerStartupPreferences startupPref) {
|
||||
[self.mockDA reset];
|
||||
|
||||
for (MockDADisk *d in disks) {
|
||||
[self.mockDA insert:d];
|
||||
}
|
||||
|
||||
deviceManager.remountArgs = remountArgs;
|
||||
|
||||
[deviceManager performStartupTasks:startupPref];
|
||||
};
|
||||
|
||||
// Unmount with RemountUSBMode set
|
||||
{
|
||||
MockDADisk *disk1 = CreateMockDisk(@"v1", @"d1");
|
||||
MockDADisk *disk2 = CreateMockDisk(@"v2", @"d2");
|
||||
|
||||
PerformStartupTest(@[ disk1, disk2 ], @[ @"noexec", @"rdonly" ],
|
||||
SNTDeviceManagerStartupPreferencesUnmount);
|
||||
|
||||
XCTAssertTrue(disk1.wasUnmounted);
|
||||
XCTAssertFalse(disk1.wasMounted);
|
||||
XCTAssertFalse(disk2.wasUnmounted);
|
||||
XCTAssertFalse(disk2.wasMounted);
|
||||
}
|
||||
|
||||
// Unmount with RemountUSBMode nil
|
||||
{
|
||||
MockDADisk *disk1 = CreateMockDisk(@"v1", @"d1");
|
||||
MockDADisk *disk2 = CreateMockDisk(@"v2", @"d2");
|
||||
|
||||
PerformStartupTest(@[ disk1, disk2 ], nil, SNTDeviceManagerStartupPreferencesUnmount);
|
||||
|
||||
XCTAssertTrue(disk1.wasUnmounted);
|
||||
XCTAssertFalse(disk1.wasMounted);
|
||||
XCTAssertTrue(disk2.wasUnmounted);
|
||||
XCTAssertFalse(disk2.wasMounted);
|
||||
}
|
||||
|
||||
// Remount with RemountUSBMode set
|
||||
{
|
||||
MockDADisk *disk1 = CreateMockDisk(@"v1", @"d1");
|
||||
MockDADisk *disk2 = CreateMockDisk(@"v2", @"d2");
|
||||
|
||||
PerformStartupTest(@[ disk1, disk2 ], @[ @"noexec", @"rdonly" ],
|
||||
SNTDeviceManagerStartupPreferencesRemount);
|
||||
|
||||
XCTAssertTrue(disk1.wasUnmounted);
|
||||
XCTAssertTrue(disk1.wasMounted);
|
||||
XCTAssertFalse(disk2.wasUnmounted);
|
||||
XCTAssertFalse(disk2.wasMounted);
|
||||
}
|
||||
|
||||
// Unmount with RemountUSBMode nil
|
||||
{
|
||||
MockDADisk *disk1 = CreateMockDisk(@"v1", @"d1");
|
||||
MockDADisk *disk2 = CreateMockDisk(@"v2", @"d2");
|
||||
|
||||
PerformStartupTest(@[ disk1, disk2 ], nil, SNTDeviceManagerStartupPreferencesRemount);
|
||||
|
||||
XCTAssertTrue(disk1.wasUnmounted);
|
||||
XCTAssertFalse(disk1.wasMounted);
|
||||
XCTAssertTrue(disk2.wasUnmounted);
|
||||
XCTAssertFalse(disk2.wasMounted);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testUpdatedMountFlags {
|
||||
struct statfs sfs;
|
||||
|
||||
strlcpy(sfs.f_fstypename, "foo", sizeof(sfs.f_fstypename));
|
||||
sfs.f_flags = MNT_JOURNALED | MNT_NOSUID | MNT_NODEV;
|
||||
|
||||
SNTEndpointSecurityDeviceManager *deviceManager = [[SNTEndpointSecurityDeviceManager alloc] init];
|
||||
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];
|
||||
|
||||
// For most filesystems, the flags are the union of what is in statfs and the remount args
|
||||
XCTAssertEqual([deviceManager updatedMountFlags:&sfs], sfs.f_flags | MNT_RDONLY | MNT_NOEXEC);
|
||||
|
||||
// For APFS, flags are still unioned, but MNT_JOUNRNALED is cleared
|
||||
strlcpy(sfs.f_fstypename, "apfs", sizeof(sfs.f_fstypename));
|
||||
XCTAssertEqual([deviceManager updatedMountFlags:&sfs],
|
||||
(sfs.f_flags | MNT_RDONLY | MNT_NOEXEC) & ~MNT_JOURNALED);
|
||||
}
|
||||
|
||||
- (void)testEnable {
|
||||
// Ensure the client subscribes to expected event types
|
||||
std::set<es_event_type_t> expectedEventSubs{
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
#include "Source/santad/TTYWriter.h"
|
||||
|
||||
typedef void (^SNTFileAccessBlockCallback)(SNTFileAccessEvent *event);
|
||||
typedef void (^SNTFileAccessBlockCallback)(SNTFileAccessEvent *event, NSString *customMsg,
|
||||
NSString *customURL, NSString *customText);
|
||||
|
||||
@interface SNTEndpointSecurityFileAccessAuthorizer
|
||||
: SNTEndpointSecurityClient <SNTEndpointSecurityDynamicEventHandler>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
@@ -32,6 +33,7 @@
|
||||
#include <variant>
|
||||
|
||||
#include "Source/common/Platform.h"
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#include "Source/common/SNTFileAccessEvent.h"
|
||||
@@ -49,8 +51,11 @@
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
|
||||
using santa::common::OptionalStringToNSString;
|
||||
using santa::common::StringToNSString;
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::FileAccessMetricStatus;
|
||||
using santa::santad::Metrics;
|
||||
using santa::santad::TTYWriter;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
using santa::santad::data_layer::WatchItemPolicy;
|
||||
@@ -76,22 +81,23 @@ struct PathTarget {
|
||||
std::optional<std::pair<dev_t, ino_t>> devnoIno;
|
||||
};
|
||||
|
||||
// This is a bespoke cache for mapping processes to a set of files the process
|
||||
// has previously been allowed to read as defined by policy. It has similar
|
||||
// semantics to SantaCache in terms of clearing the cache keys and values when
|
||||
// max sizes are reached.
|
||||
// This is a bespoke cache for mapping processes to a set of values. It has
|
||||
// similar semantics to SantaCache in terms of clearing the cache keys and
|
||||
// values when max sizes are reached.
|
||||
//
|
||||
// TODO: We need a proper LRU cache
|
||||
//
|
||||
// NB: SantaCache should not be used here.
|
||||
// 1.) It doesn't efficiently support non-primitive value types. Since the
|
||||
// value of each key needs to be a set, we want to refrain from having to
|
||||
// unnecessarily copy the value.
|
||||
// 2.) It doesn't support size limits on value types
|
||||
class ProcessFiles {
|
||||
using FileSet = absl::flat_hash_set<std::pair<dev_t, ino_t>>;
|
||||
// NB: This exists instead of using SantaCache for two main reasons:
|
||||
// 1.) SantaCache doesn't efficiently support non-primitive value types.
|
||||
// Since the value of each key needs to be a set, we want to refrain
|
||||
// from having to unnecessarily copy the value.
|
||||
// 2.) SantaCache doesn't support size limits on value types
|
||||
template <typename ValueT>
|
||||
class ProcessSet {
|
||||
using FileSet = absl::flat_hash_set<ValueT>;
|
||||
|
||||
public:
|
||||
ProcessFiles() {
|
||||
ProcessSet() {
|
||||
q_ = dispatch_queue_create(
|
||||
"com.google.santa.daemon.faa",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
|
||||
@@ -99,28 +105,15 @@ class ProcessFiles {
|
||||
};
|
||||
|
||||
// Add the given target to the set of files a process can read
|
||||
void Set(const es_process_t *proc, const PathTarget &target) {
|
||||
if (!target.devnoIno.has_value()) {
|
||||
void Set(const es_process_t *proc, std::function<ValueT()> valueBlock) {
|
||||
if (!valueBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::pair<pid_t, pid_t> pidPidver = {audit_token_to_pid(proc->audit_token),
|
||||
audit_token_to_pidversion(proc->audit_token)};
|
||||
|
||||
dispatch_sync(q_, ^{
|
||||
// If we hit the size limit, clear the cache to prevent unbounded growth
|
||||
if (cache_.size() >= kMaxCacheSize) {
|
||||
ClearLocked();
|
||||
}
|
||||
|
||||
FileSet &fs = cache_[std::move(pidPidver)];
|
||||
|
||||
// If we hit the per-entry size limit, clear the entry to prevent unbounded growth
|
||||
if (fs.size() >= kMaxCacheEntrySize) {
|
||||
fs.clear();
|
||||
}
|
||||
|
||||
fs.insert(*target.devnoIno);
|
||||
std::pair<pid_t, pid_t> pidPidver = {audit_token_to_pid(proc->audit_token),
|
||||
audit_token_to_pidversion(proc->audit_token)};
|
||||
SetLocked(pidPidver, valueBlock());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -134,22 +127,14 @@ class ProcessFiles {
|
||||
}
|
||||
|
||||
// Check if the set of files for a given process contains the given file
|
||||
bool Exists(const es_process_t *proc, const es_file_t *file) {
|
||||
std::pair<pid_t, pid_t> pidPidver = {audit_token_to_pid(proc->audit_token),
|
||||
audit_token_to_pidversion(proc->audit_token)};
|
||||
std::pair<dev_t, ino_t> devnoIno = {file->stat.st_dev, file->stat.st_ino};
|
||||
bool Exists(const es_process_t *proc, std::function<ValueT()> valueBlock) {
|
||||
return ExistsOrSet(proc, valueBlock, false);
|
||||
}
|
||||
|
||||
__block bool exists = false;
|
||||
|
||||
dispatch_sync(q_, ^{
|
||||
const auto &iter = cache_.find(pidPidver);
|
||||
|
||||
if (iter != cache_.end() && iter->second.count(devnoIno) > 0) {
|
||||
exists = true;
|
||||
}
|
||||
});
|
||||
|
||||
return exists;
|
||||
// Check if the ValueT set for a given process contains the given file, and
|
||||
// if not, set it. Both steps are done atomically.
|
||||
bool ExistsOrSet(const es_process_t *proc, std::function<ValueT()> valueBlock) {
|
||||
return ExistsOrSet(proc, valueBlock, true);
|
||||
}
|
||||
|
||||
// Clear all cache entries
|
||||
@@ -163,6 +148,42 @@ class ProcessFiles {
|
||||
// Remove everything in the cache.
|
||||
void ClearLocked() { cache_.clear(); }
|
||||
|
||||
void SetLocked(std::pair<pid_t, pid_t> pidPidver, ValueT value) {
|
||||
// If we hit the size limit, clear the cache to prevent unbounded growth
|
||||
if (cache_.size() >= kMaxCacheSize) {
|
||||
ClearLocked();
|
||||
}
|
||||
|
||||
FileSet &fs = cache_[std::move(pidPidver)];
|
||||
|
||||
// If we hit the per-entry size limit, clear the entry to prevent unbounded growth
|
||||
if (fs.size() >= kMaxCacheEntrySize) {
|
||||
fs.clear();
|
||||
}
|
||||
|
||||
fs.insert(value);
|
||||
}
|
||||
|
||||
bool ExistsOrSet(const es_process_t *proc, std::function<ValueT()> valueBlock, bool shouldSet) {
|
||||
std::pair<pid_t, pid_t> pidPidver = {audit_token_to_pid(proc->audit_token),
|
||||
audit_token_to_pidversion(proc->audit_token)};
|
||||
|
||||
__block bool exists = false;
|
||||
|
||||
dispatch_sync(q_, ^{
|
||||
ValueT value = valueBlock();
|
||||
const auto &iter = cache_.find(pidPidver);
|
||||
|
||||
if (iter != cache_.end() && iter->second.count(value) > 0) {
|
||||
exists = true;
|
||||
} else if (shouldSet) {
|
||||
SetLocked(pidPidver, value);
|
||||
}
|
||||
});
|
||||
|
||||
return exists;
|
||||
}
|
||||
|
||||
dispatch_queue_t q_;
|
||||
absl::flat_hash_map<std::pair<pid_t, pid_t>, FileSet> cache_;
|
||||
|
||||
@@ -217,6 +238,40 @@ es_auth_result_t FileAccessPolicyDecisionToESAuthResult(FileAccessPolicyDecision
|
||||
}
|
||||
}
|
||||
|
||||
bool IsBlockDecision(FileAccessPolicyDecision decision) {
|
||||
return decision == FileAccessPolicyDecision::kDenied ||
|
||||
decision == FileAccessPolicyDecision::kDeniedInvalidSignature;
|
||||
}
|
||||
|
||||
FileAccessPolicyDecision ApplyOverrideToDecision(FileAccessPolicyDecision decision,
|
||||
SNTOverrideFileAccessAction overrideAction) {
|
||||
switch (overrideAction) {
|
||||
// When no override should be applied, return the decision unmodified
|
||||
case SNTOverrideFileAccessActionNone: return decision;
|
||||
|
||||
// When the decision should be overridden to be audit only, only change the
|
||||
// decision if it was going to deny the operation.
|
||||
case SNTOverrideFileAccessActionAuditOnly:
|
||||
if (IsBlockDecision(decision)) {
|
||||
return FileAccessPolicyDecision::kAllowedAuditOnly;
|
||||
} else {
|
||||
return decision;
|
||||
}
|
||||
|
||||
// If the override action is to disable policy, return a decision that will
|
||||
// be treated as if no policy applied to the operation.
|
||||
case SNTOverrideFileAccessActionDiable: return FileAccessPolicyDecision::kNoPolicy;
|
||||
|
||||
default:
|
||||
// This is a programming error. Bail.
|
||||
LOGE(@"Invalid override file access action encountered: %d",
|
||||
static_cast<int>(overrideAction));
|
||||
[NSException
|
||||
raise:@"Invalid SNTOverrideFileAccessAction"
|
||||
format:@"Invalid SNTOverrideFileAccessAction: %d", static_cast<int>(overrideAction)];
|
||||
}
|
||||
}
|
||||
|
||||
bool ShouldLogDecision(FileAccessPolicyDecision decision) {
|
||||
switch (decision) {
|
||||
case FileAccessPolicyDecision::kDenied: return true;
|
||||
@@ -308,7 +363,23 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
}
|
||||
}
|
||||
|
||||
bool ShouldMessageTTY(const std::shared_ptr<WatchItemPolicy> &policy, const Message &msg,
|
||||
ProcessSet<std::pair<std::string, std::string>> &ttyMessageCache) {
|
||||
if (policy->silent_tty || !TTYWriter::CanWrite(msg->process)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ExistsOrSet returns `true` if the item existed. However we want to invert
|
||||
// this result as the return value for this function since we want to message
|
||||
// the TTY only when `ExistsOrSet` was a "set" operation, meaning it was the
|
||||
// first time this value was added.
|
||||
return !ttyMessageCache.ExistsOrSet(msg->process, ^std::pair<std::string, std::string>() {
|
||||
return {policy->version, policy->name};
|
||||
});
|
||||
}
|
||||
|
||||
@interface SNTEndpointSecurityFileAccessAuthorizer ()
|
||||
@property SNTConfigurator *configurator;
|
||||
@property SNTDecisionCache *decisionCache;
|
||||
@property bool isSubscribed;
|
||||
@end
|
||||
@@ -320,13 +391,15 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
std::shared_ptr<RateLimiter> _rateLimiter;
|
||||
SantaCache<SantaVnode, NSString *> _certHashCache;
|
||||
std::shared_ptr<TTYWriter> _ttyWriter;
|
||||
ProcessFiles _readsCache;
|
||||
ProcessSet<std::pair<dev_t, ino_t>> _readsCache;
|
||||
ProcessSet<std::pair<std::string, std::string>> _ttyMessageCache;
|
||||
std::shared_ptr<Metrics> _metrics;
|
||||
}
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
metrics:(std::shared_ptr<Metrics>)metrics
|
||||
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
|
||||
watchItems:(std::shared_ptr<WatchItems>)watchItems
|
||||
enricher:
|
||||
@@ -342,8 +415,11 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
_enricher = std::move(enricher);
|
||||
_decisionCache = decisionCache;
|
||||
_ttyWriter = std::move(ttyWriter);
|
||||
_metrics = std::move(metrics);
|
||||
|
||||
_rateLimiter = RateLimiter::Create(metrics, santa::santad::Processor::kFileAccessAuthorizer,
|
||||
_configurator = [SNTConfigurator configurator];
|
||||
|
||||
_rateLimiter = RateLimiter::Create(_metrics, santa::santad::Processor::kFileAccessAuthorizer,
|
||||
kDefaultRateLimitQPS);
|
||||
|
||||
SNTMetricBooleanGauge *famEnabled = [[SNTMetricSet sharedInstance]
|
||||
@@ -526,7 +602,7 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
|
||||
// If the process is signed but has an invalid signature, it is denied
|
||||
if (((msg->process->codesigning_flags & (CS_SIGNED | CS_VALID)) == CS_SIGNED) &&
|
||||
[[SNTConfigurator configurator] enableBadSignatureProtection]) {
|
||||
[self.configurator enableBadSignatureProtection]) {
|
||||
// TODO(mlw): Think about how to make stronger guarantees here to handle
|
||||
// programs becoming invalid after first being granted access. Maybe we
|
||||
// should only allow things that have hardened runtime flags set?
|
||||
@@ -536,8 +612,10 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
std::shared_ptr<WatchItemPolicy> policy = optionalPolicy.value();
|
||||
|
||||
// If policy allows reading, add target to the cache
|
||||
if (policy->allow_read_access && target.isReadable) {
|
||||
self->_readsCache.Set(msg->process, target);
|
||||
if (policy->allow_read_access && target.devnoIno.has_value()) {
|
||||
self->_readsCache.Set(msg->process, ^{
|
||||
return *target.devnoIno;
|
||||
});
|
||||
}
|
||||
|
||||
// Check if this action contains any special case that would produce
|
||||
@@ -573,10 +651,6 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
decision = FileAccessPolicyDecision::kAllowedAuditOnly;
|
||||
}
|
||||
|
||||
// https://github.com/google/santa/issues/1084
|
||||
// TODO(xyz): Write to TTY like in exec controller?
|
||||
// TODO(xyz): Need new config item for custom message in UI
|
||||
|
||||
return decision;
|
||||
}
|
||||
|
||||
@@ -584,16 +658,25 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
target:(const PathTarget &)target
|
||||
policy:
|
||||
(std::optional<std::shared_ptr<WatchItemPolicy>>)optionalPolicy
|
||||
policyVersion:(const std::string &)policyVersion {
|
||||
FileAccessPolicyDecision policyDecision = [self applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:msg];
|
||||
policyVersion:(const std::string &)policyVersion
|
||||
overrideAction:(SNTOverrideFileAccessAction)overrideAction {
|
||||
FileAccessPolicyDecision policyDecision = ApplyOverrideToDecision(
|
||||
[self applyPolicy:optionalPolicy forTarget:target toMessage:msg], overrideAction);
|
||||
|
||||
// Note: If ShouldLogDecision, it shouldn't be possible for optionalPolicy
|
||||
// to not have a value. Performing the check just in case to prevent a crash.
|
||||
if (ShouldLogDecision(policyDecision) && optionalPolicy.has_value()) {
|
||||
if (_rateLimiter->Decide(msg->mach_time) == RateLimiter::Decision::kAllowed) {
|
||||
std::string policyNameCopy = optionalPolicy.value()->name;
|
||||
std::shared_ptr<WatchItemPolicy> policy = optionalPolicy.value();
|
||||
RateLimiter::Decision decision = _rateLimiter->Decide(msg->mach_time);
|
||||
|
||||
self->_metrics->SetFileAccessEventMetrics(policyVersion, policy->name,
|
||||
(decision == RateLimiter::Decision::kAllowed)
|
||||
? FileAccessMetricStatus::kOK
|
||||
: FileAccessMetricStatus::kBlockedUser,
|
||||
msg->event_type, policyDecision);
|
||||
|
||||
if (decision == RateLimiter::Decision::kAllowed) {
|
||||
std::string policyNameCopy = policy->name;
|
||||
std::string policyVersionCopy = policyVersion;
|
||||
std::string targetPathCopy = target.path;
|
||||
|
||||
@@ -605,16 +688,18 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
targetPathCopy, policyDecision);
|
||||
}];
|
||||
}
|
||||
#if 0
|
||||
if (!optionalPolicy.value()->silent && self.fileAccessBlockCallback) {
|
||||
|
||||
// Notify users on block decisions
|
||||
if (ShouldNotifyUserDecision(policyDecision) &&
|
||||
(!policy->silent || (!policy->silent_tty && msg->process->tty->path.length > 0))) {
|
||||
SNTCachedDecision *cd =
|
||||
[self.decisionCache cachedDecisionForFile:msg->process->executable->stat];
|
||||
|
||||
SNTFileAccessEvent *event = [[SNTFileAccessEvent alloc] init];
|
||||
|
||||
event.accessedPath = StringToNSString(target.path);
|
||||
event.ruleVersion = StringToNSString(optionalPolicy.value()->version);
|
||||
event.ruleName = StringToNSString(optionalPolicy.value()->name);
|
||||
event.ruleVersion = StringToNSString(policy->version);
|
||||
event.ruleName = StringToNSString(policy->name);
|
||||
|
||||
event.fileSHA256 = cd.sha256 ?: @"<unknown sha>";
|
||||
event.filePath = StringToNSString(msg->process->executable->path.data);
|
||||
@@ -623,16 +708,50 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
event.pid = @(audit_token_to_pid(msg->process->audit_token));
|
||||
event.ppid = @(audit_token_to_pid(msg->process->parent_audit_token));
|
||||
event.parentName = StringToNSString(msg.ParentProcessName());
|
||||
event.signingChain = cd.certChain;
|
||||
|
||||
self.fileAccessBlockCallback(event);
|
||||
std::pair<NSString *, NSString *> linkInfo = self->_watchItems->EventDetailLinkInfo(policy);
|
||||
|
||||
if (!policy->silent && self.fileAccessBlockCallback) {
|
||||
self.fileAccessBlockCallback(event, OptionalStringToNSString(policy->custom_message),
|
||||
linkInfo.first, linkInfo.second);
|
||||
}
|
||||
|
||||
if (ShouldMessageTTY(policy, msg, self->_ttyMessageCache)) {
|
||||
NSAttributedString *attrStr =
|
||||
[SNTBlockMessage attributedBlockMessageForFileAccessEvent:event
|
||||
customMessage:OptionalStringToNSString(
|
||||
policy->custom_message)];
|
||||
|
||||
NSMutableString *blockMsg = [NSMutableString stringWithCapacity:1024];
|
||||
// Escape sequences `\033[1m` and `\033[0m` begin/end bold lettering
|
||||
[blockMsg appendFormat:@"\n\033[1mSanta\033[0m\n\n%@\n\n", attrStr.string];
|
||||
[blockMsg appendFormat:@"\033[1mAccessed Path:\033[0m %@\n"
|
||||
@"\033[1mRule Version: \033[0m %@\n"
|
||||
@"\033[1mRule Name: \033[0m %@\n"
|
||||
@"\n"
|
||||
@"\033[1mProcess Path: \033[0m %@\n"
|
||||
@"\033[1mIdentifier: \033[0m %@\n"
|
||||
@"\033[1mParent: \033[0m %@\n\n",
|
||||
event.accessedPath, event.ruleVersion, event.ruleName,
|
||||
event.filePath, event.fileSHA256, event.parentName];
|
||||
|
||||
NSURL *detailURL = [SNTBlockMessage eventDetailURLForFileAccessEvent:event
|
||||
customURL:linkInfo.first];
|
||||
if (detailURL) {
|
||||
[blockMsg appendFormat:@"More info:\n%@\n\n", detailURL.absoluteString];
|
||||
}
|
||||
|
||||
self->_ttyWriter->Write(msg->process, blockMsg);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
return policyDecision;
|
||||
}
|
||||
|
||||
- (void)processMessage:(const Message &)msg {
|
||||
- (void)processMessage:(const Message &)msg
|
||||
overrideAction:(SNTOverrideFileAccessAction)overrideAction {
|
||||
std::vector<PathTarget> targets;
|
||||
targets.reserve(2);
|
||||
PopulatePathTargets(msg, targets);
|
||||
@@ -654,7 +773,8 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
FileAccessPolicyDecision curDecision = [self handleMessage:msg
|
||||
target:targets[i]
|
||||
policy:versionAndPolicies.second[i]
|
||||
policyVersion:versionAndPolicies.first];
|
||||
policyVersion:versionAndPolicies.first
|
||||
overrideAction:overrideAction];
|
||||
|
||||
policyResult =
|
||||
CombinePolicyResults(policyResult, FileAccessPolicyDecisionToESAuthResult(curDecision));
|
||||
@@ -679,21 +799,35 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
|
||||
- (void)handleMessage:(santa::santad::event_providers::endpoint_security::Message &&)esMsg
|
||||
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
|
||||
SNTOverrideFileAccessAction overrideAction = [self.configurator overrideFileAccessAction];
|
||||
|
||||
// If the override action is set to Disable, return immediately.
|
||||
if (overrideAction == SNTOverrideFileAccessActionDiable) {
|
||||
if (esMsg->action_type == ES_ACTION_TYPE_AUTH) {
|
||||
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:false];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (esMsg->event_type == ES_EVENT_TYPE_AUTH_OPEN &&
|
||||
!(esMsg->event.open.fflag & kOpenFlagsIndicatingWrite)) {
|
||||
if (self->_readsCache.Exists(esMsg->process, esMsg->event.open.file)) {
|
||||
if (self->_readsCache.Exists(esMsg->process, ^std::pair<pid_t, pid_t> {
|
||||
return std::pair<dev_t, ino_t>{esMsg->event.open.file->stat.st_dev,
|
||||
esMsg->event.open.file->stat.st_ino};
|
||||
})) {
|
||||
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:false];
|
||||
return;
|
||||
}
|
||||
} else if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_EXIT) {
|
||||
// On process exit, remove the cache entry
|
||||
self->_readsCache.Remove(esMsg->process);
|
||||
self->_ttyMessageCache.Remove(esMsg->process);
|
||||
return;
|
||||
}
|
||||
|
||||
[self processMessage:std::move(esMsg)
|
||||
handler:^(const Message &msg) {
|
||||
[self processMessage:msg];
|
||||
[self processMessage:msg overrideAction:overrideAction];
|
||||
recordEventMetrics(EventDisposition::kProcessed);
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <sys/fcntl.h>
|
||||
#include <sys/types.h>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
@@ -32,6 +33,7 @@
|
||||
|
||||
#include "Source/common/Platform.h"
|
||||
#include "Source/common/SNTCachedDecision.h"
|
||||
#include "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
@@ -60,6 +62,9 @@ extern es_auth_result_t FileAccessPolicyDecisionToESAuthResult(FileAccessPolicyD
|
||||
extern bool ShouldLogDecision(FileAccessPolicyDecision decision);
|
||||
extern bool ShouldNotifyUserDecision(FileAccessPolicyDecision decision);
|
||||
extern es_auth_result_t CombinePolicyResults(es_auth_result_t result1, es_auth_result_t result2);
|
||||
extern bool IsBlockDecision(FileAccessPolicyDecision decision);
|
||||
extern FileAccessPolicyDecision ApplyOverrideToDecision(FileAccessPolicyDecision decision,
|
||||
SNTOverrideFileAccessAction overrideAction);
|
||||
|
||||
static inline std::pair<dev_t, ino_t> FileID(const es_file_t &file) {
|
||||
return std::make_pair(file.stat.st_dev, file.stat.st_ino);
|
||||
@@ -261,6 +266,63 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testIsBlockDecision {
|
||||
std::map<FileAccessPolicyDecision, bool> policyDecisionToIsBlockDecision = {
|
||||
{FileAccessPolicyDecision::kNoPolicy, false},
|
||||
{FileAccessPolicyDecision::kDenied, true},
|
||||
{FileAccessPolicyDecision::kDeniedInvalidSignature, true},
|
||||
{FileAccessPolicyDecision::kAllowed, false},
|
||||
{FileAccessPolicyDecision::kAllowedReadAccess, false},
|
||||
{FileAccessPolicyDecision::kAllowedAuditOnly, false},
|
||||
{(FileAccessPolicyDecision)123, false},
|
||||
};
|
||||
|
||||
for (const auto &kv : policyDecisionToIsBlockDecision) {
|
||||
XCTAssertEqual(ShouldNotifyUserDecision(kv.first), kv.second);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testApplyOverrideToDecision {
|
||||
std::map<std::pair<FileAccessPolicyDecision, SNTOverrideFileAccessAction>,
|
||||
FileAccessPolicyDecision>
|
||||
decisionAndOverrideToDecision = {
|
||||
// Override action: None - Policy shouldn't be changed
|
||||
{{FileAccessPolicyDecision::kNoPolicy, SNTOverrideFileAccessActionNone},
|
||||
FileAccessPolicyDecision::kNoPolicy},
|
||||
{{FileAccessPolicyDecision::kDenied, SNTOverrideFileAccessActionNone},
|
||||
FileAccessPolicyDecision::kDenied},
|
||||
|
||||
// Override action: AuditOnly - Policy should be changed only on blocked decisions
|
||||
{{FileAccessPolicyDecision::kNoPolicy, SNTOverrideFileAccessActionAuditOnly},
|
||||
FileAccessPolicyDecision::kNoPolicy},
|
||||
{{FileAccessPolicyDecision::kAllowedAuditOnly, SNTOverrideFileAccessActionAuditOnly},
|
||||
FileAccessPolicyDecision::kAllowedAuditOnly},
|
||||
{{FileAccessPolicyDecision::kAllowedReadAccess, SNTOverrideFileAccessActionAuditOnly},
|
||||
FileAccessPolicyDecision::kAllowedReadAccess},
|
||||
{{FileAccessPolicyDecision::kDenied, SNTOverrideFileAccessActionAuditOnly},
|
||||
FileAccessPolicyDecision::kAllowedAuditOnly},
|
||||
{{FileAccessPolicyDecision::kDeniedInvalidSignature, SNTOverrideFileAccessActionAuditOnly},
|
||||
FileAccessPolicyDecision::kAllowedAuditOnly},
|
||||
|
||||
// Override action: Disable - Always changes the decision to be no policy applied
|
||||
{{FileAccessPolicyDecision::kAllowed, SNTOverrideFileAccessActionDiable},
|
||||
FileAccessPolicyDecision::kNoPolicy},
|
||||
{{FileAccessPolicyDecision::kDenied, SNTOverrideFileAccessActionDiable},
|
||||
FileAccessPolicyDecision::kNoPolicy},
|
||||
{{FileAccessPolicyDecision::kAllowedReadAccess, SNTOverrideFileAccessActionDiable},
|
||||
FileAccessPolicyDecision::kNoPolicy},
|
||||
{{FileAccessPolicyDecision::kAllowedAuditOnly, SNTOverrideFileAccessActionDiable},
|
||||
FileAccessPolicyDecision::kNoPolicy},
|
||||
};
|
||||
|
||||
for (const auto &kv : decisionAndOverrideToDecision) {
|
||||
XCTAssertEqual(ApplyOverrideToDecision(kv.first.first, kv.first.second), kv.second);
|
||||
}
|
||||
|
||||
XCTAssertThrows(
|
||||
ApplyOverrideToDecision(FileAccessPolicyDecision::kAllowed, (SNTOverrideFileAccessAction)123));
|
||||
}
|
||||
|
||||
- (void)testCombinePolicyResults {
|
||||
// Ensure that the combined result is ES_AUTH_RESULT_DENY if both or either
|
||||
// input result is ES_AUTH_RESULT_DENY.
|
||||
@@ -717,7 +779,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testGetPathTargets {
|
||||
- (void)testPopulatePathTargets {
|
||||
// This test ensures that the `GetPathTargets` functions returns the
|
||||
// expected combination of targets for each handled event variant
|
||||
es_file_t testFile1 = MakeESFile("test_file_1", MakeStat(100));
|
||||
|
||||
@@ -88,9 +88,23 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
// Pre-enrichment processing
|
||||
switch (esMsg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: {
|
||||
// TODO(mlw): Once we move to building with the macOS 13 SDK, we should also check
|
||||
// the `was_mapped_writable` field
|
||||
if (esMsg->event.close.modified == false) {
|
||||
BOOL shouldLogClose = esMsg->event.close.modified;
|
||||
|
||||
#if HAVE_MACOS_13
|
||||
if (@available(macOS 13.5, *)) {
|
||||
// As of macSO 13.0 we have a new field for if a file was mmaped with
|
||||
// write permissions on close events. However it did not work until
|
||||
// 13.5.
|
||||
//
|
||||
// If something was mmaped writable it was probably written to. Often
|
||||
// developer tools do this to avoid lots of write syscalls, e.g. go's
|
||||
// tool chain. We log this so the compiler controller can take that into
|
||||
// account.
|
||||
shouldLogClose |= esMsg->event.close.was_mapped_writable;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!shouldLogClose) {
|
||||
// Ignore unmodified files
|
||||
// Note: Do not record metrics in this case. These are not considered "drops"
|
||||
// because this is not a failure case. Ideally we would tell ES to not send
|
||||
@@ -115,7 +129,6 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <objc/NSObjCRuntime.h>
|
||||
#include <cstddef>
|
||||
|
||||
#include <memory>
|
||||
@@ -102,12 +103,21 @@ class MockLogger : public Logger {
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testHandleMessage {
|
||||
typedef void (^testHelperBlock)(es_message_t *message,
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient,
|
||||
std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
dispatch_semaphore_t *sema, dispatch_semaphore_t *semaMetrics);
|
||||
|
||||
es_file_t targetFileMatchesRegex = MakeESFile("/foo/matches");
|
||||
es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
|
||||
- (void)handleMessageWithMatchCalls:(BOOL)regexMatchCalls
|
||||
withMissCalls:(BOOL)regexFailsMatchCalls
|
||||
withBlock:(testHelperBlock)testBlock {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc, ActionType::Auth);
|
||||
es_file_t targetFileMatchesRegex = MakeESFile("/foo/matches");
|
||||
es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
@@ -116,11 +126,15 @@ class MockLogger : public Logger {
|
||||
std::unique_ptr<EnrichedMessage> enrichedMsg = std::unique_ptr<EnrichedMessage>(nullptr);
|
||||
|
||||
auto mockEnricher = std::make_shared<MockEnricher>();
|
||||
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(std::move(enrichedMsg)));
|
||||
|
||||
if (regexMatchCalls) {
|
||||
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(std::move(enrichedMsg)));
|
||||
}
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMatchesRegex)).Times(1);
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMissesRegex)).Times(1);
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMatchesRegex)).Times((int)regexMatchCalls);
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMissesRegex))
|
||||
.Times((int)regexFailsMatchCalls);
|
||||
|
||||
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
|
||||
|
||||
@@ -128,11 +142,13 @@ class MockLogger : public Logger {
|
||||
// `SNTEndpointSecurityRecorder` object. There is a bug in OCMock that doesn't
|
||||
// properly handle the `processEnrichedMessage:handler:` block. Instead this
|
||||
// test will mock the `Log` method that is called in the handler block.
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
auto mockLogger = std::make_shared<MockLogger>(nullptr, nullptr);
|
||||
EXPECT_CALL(*mockLogger, Log).WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}));
|
||||
if (regexMatchCalls) {
|
||||
EXPECT_CALL(*mockLogger, Log).WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}));
|
||||
}
|
||||
|
||||
auto prefixTree = std::make_shared<PrefixTree<Unit>>();
|
||||
|
||||
@@ -147,66 +163,7 @@ class MockLogger : public Logger {
|
||||
authResultCache:mockAuthCache
|
||||
prefixTree:prefixTree];
|
||||
|
||||
// CLOSE not modified, bail early
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg.event.close.modified = false;
|
||||
esMsg.event.close.target = NULL;
|
||||
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, &esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTFail("Metrics record callback should not be called here");
|
||||
}]);
|
||||
}
|
||||
|
||||
// CLOSE modified, remove from cache, and matches fileChangesRegex
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg.event.close.modified = true;
|
||||
esMsg.event.close.target = &targetFileMatchesRegex;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
|
||||
[recorderClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kProcessed);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertSemaTrue(sema, 5, "Log wasn't called within expected time window");
|
||||
}
|
||||
|
||||
// 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 = &targetFileMatchesRegex;
|
||||
prefixTree->InsertPrefix(esMsg.event.link.source->path.data, Unit{});
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
|
||||
[recorderClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
}
|
||||
testBlock(&esMsg, mockESApi, mockCC, recorderClient, prefixTree, &sema, &semaMetrics);
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockCC));
|
||||
|
||||
@@ -218,6 +175,150 @@ class MockLogger : public Logger {
|
||||
[mockCC stopMocking];
|
||||
}
|
||||
|
||||
- (void)testHandleMessageWithCloseMappedWriteable {
|
||||
#if HAVE_MACOS_13
|
||||
if (@available(macOS 13.0, *)) {
|
||||
// CLOSE not modified, but was_mapped_writable, should remove from cache,
|
||||
// and matches fileChangesRegex
|
||||
testHelperBlock testBlock =
|
||||
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema,
|
||||
__autoreleasing dispatch_semaphore_t *semaMetrics) {
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg->event.close.modified = false;
|
||||
esMsg->event.close.was_mapped_writable = true;
|
||||
esMsg->event.close.target = &targetFileMatchesRegex;
|
||||
Message msg(mockESApi, esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kProcessed);
|
||||
dispatch_semaphore_signal(*semaMetrics);
|
||||
}]);
|
||||
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertSemaTrue(*sema, 5, "Log wasn't called within expected time window");
|
||||
};
|
||||
|
||||
[self handleMessageWithMatchCalls:YES withMissCalls:NO withBlock:testBlock];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)testHandleEventCloseNotModifiedWithWasMappedWritable {
|
||||
#if HAVE_MACOS_13
|
||||
if (@available(macOS 13.0, *)) {
|
||||
// CLOSE not modified, but was_mapped_writable, remove from cache, and does not match
|
||||
// fileChangesRegex
|
||||
testHelperBlock testBlock =
|
||||
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema,
|
||||
__autoreleasing dispatch_semaphore_t *semaMetrics) {
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg->event.close.modified = false;
|
||||
esMsg->event.close.was_mapped_writable = true;
|
||||
esMsg->event.close.target = &targetFileMissesRegex;
|
||||
Message msg(mockESApi, esMsg);
|
||||
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTFail("Metrics record callback should not be called here");
|
||||
}]);
|
||||
};
|
||||
|
||||
[self handleMessageWithMatchCalls:NO withMissCalls:YES withBlock:testBlock];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)testHandleMessage {
|
||||
// CLOSE not modified, bail early
|
||||
testHelperBlock testBlock = ^(
|
||||
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg->event.close.modified = false;
|
||||
esMsg->event.close.target = NULL;
|
||||
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTFail("Metrics record callback should not be called here");
|
||||
}]);
|
||||
};
|
||||
|
||||
[self handleMessageWithMatchCalls:NO withMissCalls:NO withBlock:testBlock];
|
||||
|
||||
// CLOSE modified, remove from cache, and matches fileChangesRegex
|
||||
testBlock = ^(
|
||||
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg->event.close.modified = true;
|
||||
esMsg->event.close.target = &targetFileMatchesRegex;
|
||||
Message msg(mockESApi, esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
|
||||
[recorderClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kProcessed);
|
||||
dispatch_semaphore_signal(*semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertSemaTrue(*sema, 5, "Log wasn't called within expected time window");
|
||||
};
|
||||
|
||||
[self handleMessageWithMatchCalls:YES withMissCalls:NO withBlock:testBlock];
|
||||
|
||||
// CLOSE modified, remove from cache, but doesn't match fileChangesRegex
|
||||
testBlock = ^(
|
||||
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg->event.close.modified = true;
|
||||
esMsg->event.close.target = &targetFileMissesRegex;
|
||||
Message msg(mockESApi, esMsg);
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTFail("Metrics record callback should not be called here");
|
||||
}]);
|
||||
};
|
||||
|
||||
[self handleMessageWithMatchCalls:NO withMissCalls:YES withBlock:testBlock];
|
||||
|
||||
// LINK, Prefix match, bail early
|
||||
testBlock =
|
||||
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics)
|
||||
|
||||
{
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_LINK;
|
||||
esMsg->event.link.source = &targetFileMatchesRegex;
|
||||
prefixTree->InsertPrefix(esMsg->event.link.source->path.data, Unit{});
|
||||
Message msg(mockESApi, esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
|
||||
[recorderClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(*semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
};
|
||||
|
||||
[self handleMessageWithMatchCalls:NO withMissCalls:NO withBlock:testBlock];
|
||||
}
|
||||
|
||||
- (void)testGetTargetFileForPrefixTree {
|
||||
// Ensure `GetTargetFileForPrefixTree` returns expected field for each
|
||||
// subscribed event type in the `SNTEndpointSecurityRecorder`.
|
||||
|
||||
@@ -55,6 +55,8 @@ class BasicString : public Serializer {
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated &) override;
|
||||
|
||||
std::vector<uint8_t> SerializeFileAccess(
|
||||
const std::string &policy_version, const std::string &policy_name,
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExec;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExit;
|
||||
@@ -46,6 +47,7 @@ using santa::santad::event_providers::endpoint_security::EnrichedProcess;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedRename;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::MountFromName;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::NonNull;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::Pid;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::Pidversion;
|
||||
@@ -411,6 +413,19 @@ std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedUnlink &msg) {
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedCSInvalidated &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=CODESIGNING_INVALIDATED");
|
||||
AppendProcess(str, esm.process);
|
||||
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
|
||||
msg.instigator().real_group());
|
||||
str.append("|codesigning_flags=");
|
||||
str.append([NSString stringWithFormat:@"0x%08x", esm.process->codesigning_flags].UTF8String);
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeFileAccess(const std::string &policy_version,
|
||||
const std::string &policy_name,
|
||||
const Message &msg,
|
||||
@@ -509,6 +524,8 @@ std::vector<uint8_t> BasicString::SerializeDiskAppeared(NSDictionary *props) {
|
||||
str.append([NonNull(dmg_path) UTF8String]);
|
||||
str.append("|appearance=");
|
||||
str.append([NonNull(appearanceDateString) UTF8String]);
|
||||
str.append("|mountfrom=");
|
||||
str.append([NonNull(MountFromName([props[@"DAVolumePath"] path])) UTF8String]);
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
@@ -251,6 +251,20 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageCSInvalidated {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED, &proc);
|
||||
|
||||
std::string got = BasicStringSerializeMessage(&esMsg);
|
||||
std::string want =
|
||||
"action=CODESIGNING_INVALIDATED"
|
||||
"|pid=12|ppid=56|process=foo|processpath=foo"
|
||||
"|uid=-2|user=nobody|gid=-1|group=nogroup|codesigning_flags=0x00000000|machineid=my_id\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testGetAccessTypeString {
|
||||
std::map<es_event_type_t, std::string> accessTypeToString = {
|
||||
{ES_EVENT_TYPE_AUTH_OPEN, "OPEN"}, {ES_EVENT_TYPE_AUTH_LINK, "LINK"},
|
||||
@@ -351,7 +365,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
@"DADeviceVendor" : @"vendor",
|
||||
@"DADeviceModel" : @"model",
|
||||
@"DAAppearanceTime" : @(1252487349), // 2009-09-09 09:09:09
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"path"],
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"/"],
|
||||
@"DAMediaBSDName" : @"bsd",
|
||||
@"DAVolumeKind" : @"apfs",
|
||||
@"DADeviceProtocol" : @"usb",
|
||||
@@ -365,11 +379,11 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
std::vector<uint8_t> ret = BasicString::Create(nullptr, nil, false)->SerializeDiskAppeared(props);
|
||||
std::string got(ret.begin(), ret.end());
|
||||
|
||||
std::string want = "action=DISKAPPEAR|mount=path|volume=|bsdname=bsd|fs=apfs"
|
||||
std::string want = "action=DISKAPPEAR|mount=/|volume=|bsdname=bsd|fs=apfs"
|
||||
"|model=vendor model|serial=|bus=usb|dmgpath="
|
||||
"|appearance=2040-09-09T09:09:09.000Z\n";
|
||||
"|appearance=2040-09-09T09:09:09.000Z|mountfrom=/";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
XCTAssertCppStringBeginsWith(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeDiskDisappeared {
|
||||
|
||||
@@ -47,6 +47,8 @@ class Empty : public Serializer {
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated &) override;
|
||||
|
||||
std::vector<uint8_t> SerializeFileAccess(
|
||||
const std::string &policy_version, const std::string &policy_name,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExec;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExit;
|
||||
@@ -65,6 +66,10 @@ std::vector<uint8_t> Empty::SerializeMessage(const EnrichedUnlink &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedCSInvalidated &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeFileAccess(const std::string &policy_version,
|
||||
const std::string &policy_name, const Message &msg,
|
||||
const EnrichedProcess &enriched_process,
|
||||
|
||||
@@ -43,6 +43,7 @@ namespace es = santa::santad::event_providers::endpoint_security;
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedLink *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedRename *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedUnlink *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedCSInvalidated *)&fake).size(), 0);
|
||||
|
||||
XCTAssertEqual(e->SerializeAllowlist(*(es::Message *)&fake, "").size(), 0);
|
||||
XCTAssertEqual(e->SerializeBundleHashingEvent(nil).size(), 0);
|
||||
|
||||
@@ -56,6 +56,8 @@ class Protobuf : public Serializer {
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated &) override;
|
||||
|
||||
std::vector<uint8_t> SerializeFileAccess(
|
||||
const std::string &policy_version, const std::string &policy_name,
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
#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 <google/protobuf/json/json.h>
|
||||
#include <mach/message.h>
|
||||
#include <math.h>
|
||||
#include <sys/proc_info.h>
|
||||
@@ -36,16 +35,18 @@
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "google/protobuf/timestamp.pb.h"
|
||||
|
||||
using google::protobuf::Arena;
|
||||
using google::protobuf::Timestamp;
|
||||
using google::protobuf::util::JsonPrintOptions;
|
||||
using google::protobuf::util::MessageToJsonString;
|
||||
using JsonPrintOptions = google::protobuf::json::PrintOptions;
|
||||
using google::protobuf::json::MessageToJsonString;
|
||||
|
||||
using santa::common::NSStringToUTF8StringView;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExec;
|
||||
@@ -59,6 +60,7 @@ using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::EffectiveGroup;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::EffectiveUser;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::MountFromName;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::NonNull;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::Pid;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::Pidversion;
|
||||
@@ -400,7 +402,7 @@ std::vector<uint8_t> Protobuf::FinalizeProto(::pbv1::SantaMessage *santa_msg) {
|
||||
options.preserve_proto_field_names = true;
|
||||
std::string json;
|
||||
|
||||
google::protobuf::util::Status status = MessageToJsonString(*santa_msg, &json, options);
|
||||
absl::Status status = MessageToJsonString(*santa_msg, &json, options);
|
||||
|
||||
if (!status.ok()) {
|
||||
LOGE(@"Failed to convert protobuf to JSON: %s", status.ToString().c_str());
|
||||
@@ -599,6 +601,17 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedUnlink &msg) {
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedCSInvalidated &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::CodesigningInvalidated *pb_cs_invalidated = santa_msg->mutable_codesigning_invalidated();
|
||||
EncodeProcessInfoLight(pb_cs_invalidated->mutable_instigator(), msg.es_msg().version,
|
||||
msg.es_msg().process, msg.instigator());
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeFileAccess(const std::string &policy_version,
|
||||
const std::string &policy_name,
|
||||
const Message &msg,
|
||||
@@ -682,6 +695,8 @@ static void EncodeDisk(::pbv1::Disk *pb_disk, ::pbv1::Disk_Action action, NSDict
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_serial(); }, serial);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_bus(); }, props[@"DADeviceProtocol"]);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_dmg_path(); }, dmg_path);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_mount_from(); },
|
||||
MountFromName([props[@"DAVolumePath"] path]));
|
||||
|
||||
if (props[@"DAAppearanceTime"]) {
|
||||
// Note: `DAAppearanceTime` is set via `CFAbsoluteTimeGetCurrent`, which uses the defined
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <google/protobuf/util/json_util.h>
|
||||
#include <google/protobuf/json/json.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/proc_info.h>
|
||||
#include <sys/signal.h>
|
||||
@@ -40,12 +40,15 @@
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "google/protobuf/any.pb.h"
|
||||
#include "google/protobuf/timestamp.pb.h"
|
||||
|
||||
using google::protobuf::Timestamp;
|
||||
using google::protobuf::util::JsonPrintOptions;
|
||||
using google::protobuf::util::JsonStringToMessage;
|
||||
using JsonPrintOptions = google::protobuf::json::PrintOptions;
|
||||
using JsonParseOptions = ::google::protobuf::json::ParseOptions;
|
||||
using google::protobuf::json::JsonStringToMessage;
|
||||
using google::protobuf::json::MessageToJsonString;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
@@ -107,6 +110,7 @@ NSString *EventTypeToFilename(es_event_type_t eventType) {
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK: return @"link.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: return @"rename.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK: return @"unlink.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED: return @"cs_invalidated.json";
|
||||
default: XCTFail(@"Unhandled event type: %d", eventType); return nil;
|
||||
}
|
||||
}
|
||||
@@ -142,6 +146,7 @@ const google::protobuf::Message &SantaMessageEvent(const ::pbv1::SantaMessage &s
|
||||
case ::pbv1::SantaMessage::kBundle: return santaMsg.bundle();
|
||||
case ::pbv1::SantaMessage::kAllowlist: return santaMsg.allowlist();
|
||||
case ::pbv1::SantaMessage::kFileAccess: return santaMsg.file_access();
|
||||
case ::pbv1::SantaMessage::kCodesigningInvalidated: return santaMsg.codesigning_invalidated();
|
||||
case ::pbv1::SantaMessage::EVENT_NOT_SET:
|
||||
XCTFail(@"Protobuf message SantaMessage did not set an 'event' field");
|
||||
OS_FALLTHROUGH;
|
||||
@@ -157,7 +162,7 @@ std::string ConvertMessageToJsonString(const ::pbv1::SantaMessage &santaMsg) {
|
||||
const google::protobuf::Message &message = SantaMessageEvent(santaMsg);
|
||||
|
||||
std::string json;
|
||||
XCTAssertTrue(google::protobuf::util::MessageToJsonString(message, &json, options).ok());
|
||||
XCTAssertTrue(MessageToJsonString(message, &json, options).ok());
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -236,9 +241,10 @@ void SerializeAndCheck(es_event_type_t eventType,
|
||||
if (json) {
|
||||
// Parse the jsonified string into the protobuf
|
||||
// gotData = protoStr;
|
||||
google::protobuf::util::JsonParseOptions options;
|
||||
JsonParseOptions options;
|
||||
options.ignore_unknown_fields = true;
|
||||
google::protobuf::util::Status status = JsonStringToMessage(protoStr, &santaMsg, options);
|
||||
absl::Status status = JsonStringToMessage(protoStr, &santaMsg, options);
|
||||
XCTAssertTrue(status.ok());
|
||||
gotData = ConvertMessageToJsonString(santaMsg);
|
||||
} else {
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
@@ -664,6 +670,14 @@ void SerializeAndCheckNonESEvents(
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageCodesigningInvalidated {
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
}
|
||||
json:NO];
|
||||
}
|
||||
|
||||
- (void)testGetAccessType {
|
||||
std::map<es_event_type_t, ::pbv1::FileAccess::AccessType> eventTypeToAccessType = {
|
||||
{ES_EVENT_TYPE_AUTH_CLONE, ::pbv1::FileAccess::ACCESS_TYPE_CLONE},
|
||||
@@ -767,7 +781,7 @@ void SerializeAndCheckNonESEvents(
|
||||
@"DADeviceVendor" : @"vendor",
|
||||
@"DADeviceModel" : @"model",
|
||||
@"DAAppearanceTime" : @(123456789),
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"path"],
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"/"],
|
||||
@"DAMediaBSDName" : @"bsd",
|
||||
@"DAVolumeKind" : @"apfs",
|
||||
@"DADeviceProtocol" : @"usb",
|
||||
@@ -792,6 +806,7 @@ void SerializeAndCheckNonESEvents(
|
||||
XCTAssertEqualObjects(@(pbDisk.serial().c_str()), @"");
|
||||
XCTAssertEqualObjects(@(pbDisk.bus().c_str()), props[@"DADeviceProtocol"]);
|
||||
XCTAssertEqualObjects(@(pbDisk.dmg_path().c_str()), @"");
|
||||
XCTAssertCppStringBeginsWith(pbDisk.mount_from(), std::string("/"));
|
||||
|
||||
// Note: `DAAppearanceTime` is treated as a reference time since 2001 and is converted to a
|
||||
// reference time of 1970. Skip the calculation in the test here, just ensure the value is set.
|
||||
|
||||
@@ -61,6 +61,8 @@ class Serializer {
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &) = 0;
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) = 0;
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated &) = 0;
|
||||
|
||||
virtual std::vector<uint8_t> SerializeFileAccess(
|
||||
const std::string &policy_version, const std::string &policy_name,
|
||||
@@ -95,6 +97,8 @@ class Serializer {
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &);
|
||||
std::vector<uint8_t> SerializeMessageTemplate(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &);
|
||||
std::vector<uint8_t> SerializeMessageTemplate(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated &);
|
||||
|
||||
bool enabled_machine_id_ = false;
|
||||
std::string machine_id_;
|
||||
|
||||
@@ -74,5 +74,8 @@ std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedRena
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedUnlink &msg) {
|
||||
return SerializeMessage(msg);
|
||||
}
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedCSInvalidated &msg) {
|
||||
return SerializeMessage(msg);
|
||||
}
|
||||
|
||||
}; // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
@@ -54,6 +54,8 @@ static inline NSString *NonNull(NSString *str) {
|
||||
NSString *OriginalPathForTranslocation(const es_process_t *es_proc);
|
||||
NSString *SerialForDevice(NSString *devPath);
|
||||
NSString *DiskImageForDevice(NSString *devPath);
|
||||
NSString *MountFromName(NSString *path);
|
||||
|
||||
es_file_t *GetAllowListTargetFile(
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg);
|
||||
|
||||
|
||||
@@ -14,9 +14,13 @@
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
|
||||
|
||||
#include <sys/mount.h>
|
||||
#include <sys/param.h>
|
||||
|
||||
#include "Source/common/SantaCache.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
#include "Source/common/SantaVnodeHash.h"
|
||||
#include "Source/common/String.h"
|
||||
|
||||
// These functions are exported by the Security framework, but are not included in headers
|
||||
extern "C" Boolean SecTranslocateIsTranslocatedURL(CFURLRef path, bool *isTranslocated,
|
||||
@@ -114,6 +118,22 @@ NSString *SerialForDevice(NSString *devPath) {
|
||||
return [serial stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
}
|
||||
|
||||
NSString *MountFromName(NSString *path) {
|
||||
if (!path.length) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
struct statfs sfs;
|
||||
|
||||
if (statfs(path.UTF8String, &sfs) != 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *mntFromName = santa::common::StringToNSString(sfs.f_mntfromname);
|
||||
|
||||
return mntFromName.length > 0 ? mntFromName : nil;
|
||||
}
|
||||
|
||||
NSString *DiskImageForDevice(NSString *devPath) {
|
||||
devPath = [devPath stringByDeletingLastPathComponent];
|
||||
if (!devPath.length) {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::GetAllowListTargetFile;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::MountFromName;
|
||||
|
||||
@interface UtilitiesTest : XCTestCase
|
||||
@end
|
||||
@@ -62,4 +63,12 @@ using santa::santad::logs::endpoint_security::serializers::Utilities::GetAllowLi
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testMountFromName {
|
||||
XCTAssertNil(MountFromName(@""));
|
||||
XCTAssertNil(MountFromName(nil));
|
||||
XCTAssertNil(MountFromName(@"./this/path/should/not/ever/exist/"));
|
||||
|
||||
XCTAssertCppStringBeginsWith(std::string(MountFromName(@"/").UTF8String), std::string("/"));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -10,6 +10,7 @@ proto_library(
|
||||
srcs = ["binaryproto.proto"],
|
||||
deps = [
|
||||
"@com_google_protobuf//:any_proto",
|
||||
"@com_google_protobuf//:timestamp_proto",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
|
||||
namespace santa::santad {
|
||||
@@ -44,8 +46,18 @@ enum class Processor {
|
||||
kFileAccessAuthorizer,
|
||||
};
|
||||
|
||||
enum class FileAccessMetricStatus {
|
||||
kOK = 0,
|
||||
kBlockedUser,
|
||||
};
|
||||
|
||||
using EventCountTuple = std::tuple<Processor, es_event_type_t, EventDisposition>;
|
||||
using EventTimesTuple = std::tuple<Processor, es_event_type_t>;
|
||||
using FileAccessMetricsPolicyVersion = std::string;
|
||||
using FileAccessMetricsPolicyName = std::string;
|
||||
using FileAccessEventCountTuple =
|
||||
std::tuple<FileAccessMetricsPolicyVersion, FileAccessMetricsPolicyName, FileAccessMetricStatus,
|
||||
es_event_type_t, FileAccessPolicyDecision>;
|
||||
|
||||
class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
public:
|
||||
@@ -53,8 +65,8 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
|
||||
Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
|
||||
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
|
||||
SNTMetricCounter *rate_limit_counts, SNTMetricSet *metric_set,
|
||||
void (^run_on_first_start)(Metrics *));
|
||||
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *faa_event_counts,
|
||||
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *));
|
||||
|
||||
~Metrics();
|
||||
|
||||
@@ -71,6 +83,10 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
|
||||
void SetRateLimitingMetrics(Processor processor, int64_t events_rate_limited_count);
|
||||
|
||||
void SetFileAccessEventMetrics(std::string policy_version, std::string rule_name,
|
||||
FileAccessMetricStatus status, es_event_type_t event_type,
|
||||
FileAccessPolicyDecision decision);
|
||||
|
||||
friend class santa::santad::MetricsPeer;
|
||||
|
||||
private:
|
||||
@@ -84,6 +100,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
SNTMetricInt64Gauge *event_processing_times_;
|
||||
SNTMetricCounter *event_counts_;
|
||||
SNTMetricCounter *rate_limit_counts_;
|
||||
SNTMetricCounter *faa_event_counts_;
|
||||
SNTMetricSet *metric_set_;
|
||||
// Tracks whether or not the timer_source should be running.
|
||||
// This helps manage dispatch source state to ensure the source is not
|
||||
@@ -99,6 +116,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
std::map<EventCountTuple, int64_t> event_counts_cache_;
|
||||
std::map<EventTimesTuple, int64_t> event_times_cache_;
|
||||
std::map<Processor, int64_t> rate_limit_counts_cache_;
|
||||
std::map<FileAccessEventCountTuple, int64_t> faa_event_counts_cache_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad
|
||||
|
||||
@@ -53,6 +53,16 @@ static NSString *const kEventTypeNotifyUnmount = @"NotifyUnmount";
|
||||
static NSString *const kEventDispositionDropped = @"Dropped";
|
||||
static NSString *const kEventDispositionProcessed = @"Processed";
|
||||
|
||||
// Compat values
|
||||
static NSString *const kFileAccessMetricStatusOK = @"OK";
|
||||
static NSString *const kFileAccessMetricStatusBlockedUser = @"BLOCKED_USER";
|
||||
|
||||
static NSString *const kFileAccessPolicyDecisionDenied = @"Denied";
|
||||
static NSString *const kFileAccessPolicyDecisionDeniedInvalidSignature = @"Denied";
|
||||
static NSString *const kFileAccessPolicyDecisionAllowedAuditOnly = @"AllowedAuditOnly";
|
||||
|
||||
static NSString *const kFileAccessMetricsAccessType = @"access";
|
||||
|
||||
namespace santa::santad {
|
||||
|
||||
NSString *const ProcessorToString(Processor processor) {
|
||||
@@ -110,6 +120,32 @@ NSString *const EventDispositionToString(EventDisposition d) {
|
||||
}
|
||||
}
|
||||
|
||||
NSString *const FileAccessMetricStatusToString(FileAccessMetricStatus status) {
|
||||
switch (status) {
|
||||
case FileAccessMetricStatus::kOK: return kFileAccessMetricStatusOK;
|
||||
case FileAccessMetricStatus::kBlockedUser: return kFileAccessMetricStatusBlockedUser;
|
||||
default:
|
||||
[NSException raise:@"Invalid file access metric status"
|
||||
format:@"Unknown file access metric status value: %d", static_cast<int>(status)];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSString *const FileAccessPolicyDecisionToString(FileAccessPolicyDecision decision) {
|
||||
switch (decision) {
|
||||
case FileAccessPolicyDecision::kDenied: return kFileAccessPolicyDecisionDenied;
|
||||
case FileAccessPolicyDecision::kDeniedInvalidSignature:
|
||||
return kFileAccessPolicyDecisionDeniedInvalidSignature;
|
||||
case FileAccessPolicyDecision::kAllowedAuditOnly:
|
||||
return kFileAccessPolicyDecisionAllowedAuditOnly;
|
||||
default:
|
||||
[NSException
|
||||
raise:@"Invalid file access policy decision"
|
||||
format:@"Unknown file access policy decision value: %d", static_cast<int>(decision)];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t interval) {
|
||||
dispatch_queue_t q = dispatch_queue_create("com.google.santa.santametricsservice.q",
|
||||
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
|
||||
@@ -131,9 +167,16 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
|
||||
fieldNames:@[ @"Processor" ]
|
||||
helpText:@"Events rate limited by each processor"];
|
||||
|
||||
SNTMetricCounter *faa_event_counts = [metric_set
|
||||
counterWithName:@"/santa/file_access_authorizer/log/count"
|
||||
fieldNames:@[
|
||||
@"config_version", @"access_type", @"rule_id", @"status", @"operation", @"decision"
|
||||
]
|
||||
helpText:@"Count of times a log is emitted from the File Access Authorizer client"];
|
||||
|
||||
std::shared_ptr<Metrics> metrics =
|
||||
std::make_shared<Metrics>(q, timer_source, interval, event_processing_times, event_counts,
|
||||
rate_limit_counts, metric_set, ^(Metrics *metrics) {
|
||||
rate_limit_counts, faa_event_counts, metric_set, ^(Metrics *metrics) {
|
||||
SNTRegisterCoreMetrics();
|
||||
metrics->EstablishConnection();
|
||||
});
|
||||
@@ -153,14 +196,15 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
|
||||
|
||||
Metrics::Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
|
||||
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
|
||||
SNTMetricCounter *rate_limit_counts, SNTMetricSet *metric_set,
|
||||
void (^run_on_first_start)(Metrics *))
|
||||
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *faa_event_counts,
|
||||
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *))
|
||||
: q_(q),
|
||||
timer_source_(timer_source),
|
||||
interval_(interval),
|
||||
event_processing_times_(event_processing_times),
|
||||
event_counts_(event_counts),
|
||||
rate_limit_counts_(rate_limit_counts),
|
||||
faa_event_counts_(faa_event_counts),
|
||||
metric_set_(metric_set),
|
||||
run_on_first_start_(run_on_first_start) {
|
||||
SetInterval(interval_);
|
||||
@@ -226,10 +270,26 @@ void Metrics::FlushMetrics() {
|
||||
[rate_limit_counts_ incrementBy:kv.second forFieldValues:@[ processorName ]];
|
||||
}
|
||||
|
||||
for (const auto &kv : faa_event_counts_cache_) {
|
||||
NSString *policyVersion = @(std::get<0>(kv.first).c_str()); // FileAccessMetricsPolicyVersion
|
||||
NSString *policyName = @(std::get<1>(kv.first).c_str()); // FileAccessMetricsPolicyName
|
||||
NSString *eventName = EventTypeToString(std::get<es_event_type_t>(kv.first));
|
||||
NSString *status = FileAccessMetricStatusToString(std::get<FileAccessMetricStatus>(kv.first));
|
||||
NSString *decision =
|
||||
FileAccessPolicyDecisionToString(std::get<FileAccessPolicyDecision>(kv.first));
|
||||
|
||||
[faa_event_counts_
|
||||
incrementBy:kv.second
|
||||
forFieldValues:@[
|
||||
policyVersion, kFileAccessMetricsAccessType, policyName, status, eventName, decision
|
||||
]];
|
||||
}
|
||||
|
||||
// Reset the maps so the next cycle begins with a clean state
|
||||
event_counts_cache_ = {};
|
||||
event_times_cache_ = {};
|
||||
rate_limit_counts_cache_ = {};
|
||||
faa_event_counts_cache_ = {};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -285,4 +345,13 @@ void Metrics::SetRateLimitingMetrics(Processor processor, int64_t events_rate_li
|
||||
});
|
||||
}
|
||||
|
||||
void Metrics::SetFileAccessEventMetrics(std::string policy_version, std::string rule_name,
|
||||
FileAccessMetricStatus status, es_event_type_t event_type,
|
||||
FileAccessPolicyDecision decision) {
|
||||
dispatch_sync(events_q_, ^{
|
||||
faa_event_counts_cache_[FileAccessEventCountTuple{
|
||||
std::move(policy_version), std::move(rule_name), status, event_type, decision}]++;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace santa::santad
|
||||
|
||||
@@ -24,37 +24,44 @@
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
using santa::santad::EventCountTuple;
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::EventTimesTuple;
|
||||
using santa::santad::FileAccessEventCountTuple;
|
||||
using santa::santad::Processor;
|
||||
|
||||
using EventCountTuple = std::tuple<Processor, es_event_type_t, EventDisposition>;
|
||||
using EventTimesTuple = std::tuple<Processor, es_event_type_t>;
|
||||
|
||||
namespace santa::santad {
|
||||
|
||||
extern NSString *const ProcessorToString(Processor processor);
|
||||
extern NSString *const EventTypeToString(es_event_type_t eventType);
|
||||
extern NSString *const EventDispositionToString(EventDisposition d);
|
||||
extern NSString *const FileAccessMetricStatusToString(FileAccessMetricStatus status);
|
||||
extern NSString *const FileAccessPolicyDecisionToString(FileAccessPolicyDecision decision);
|
||||
|
||||
class MetricsPeer : public Metrics {
|
||||
public:
|
||||
// Make base class constructors visible
|
||||
using Metrics::FlushMetrics;
|
||||
using Metrics::Metrics;
|
||||
|
||||
bool IsRunning() { return running_; }
|
||||
// Private methods
|
||||
using Metrics::FlushMetrics;
|
||||
|
||||
uint64_t Interval() { return interval_; }
|
||||
|
||||
std::map<EventCountTuple, int64_t> &EventCounts() { return event_counts_cache_; };
|
||||
std::map<EventTimesTuple, int64_t> &EventTimes() { return event_times_cache_; };
|
||||
std::map<Processor, int64_t> &RateLimitCounts() { return rate_limit_counts_cache_; };
|
||||
// Private member variables
|
||||
using Metrics::event_counts_cache_;
|
||||
using Metrics::event_times_cache_;
|
||||
using Metrics::faa_event_counts_cache_;
|
||||
using Metrics::interval_;
|
||||
using Metrics::rate_limit_counts_cache_;
|
||||
using Metrics::running_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad
|
||||
|
||||
using santa::santad::EventDispositionToString;
|
||||
using santa::santad::EventTypeToString;
|
||||
using santa::santad::FileAccessMetricStatus;
|
||||
using santa::santad::FileAccessMetricStatusToString;
|
||||
using santa::santad::FileAccessPolicyDecisionToString;
|
||||
using santa::santad::MetricsPeer;
|
||||
using santa::santad::ProcessorToString;
|
||||
|
||||
@@ -73,19 +80,19 @@ using santa::santad::ProcessorToString;
|
||||
|
||||
- (void)testStartStop {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m) {
|
||||
dispatch_semaphore_signal(self.sema);
|
||||
});
|
||||
|
||||
XCTAssertFalse(metrics->IsRunning());
|
||||
XCTAssertFalse(metrics->running_);
|
||||
|
||||
metrics->StartPoll();
|
||||
XCTAssertEqual(0, dispatch_semaphore_wait(self.sema, DISPATCH_TIME_NOW),
|
||||
"Initialization block never called");
|
||||
|
||||
// Should be marked running after starting
|
||||
XCTAssertTrue(metrics->IsRunning());
|
||||
XCTAssertTrue(metrics->running_);
|
||||
|
||||
metrics->StartPoll();
|
||||
|
||||
@@ -94,29 +101,29 @@ using santa::santad::ProcessorToString;
|
||||
"Initialization block called second time unexpectedly");
|
||||
|
||||
// Double-start doesn't change the running state
|
||||
XCTAssertTrue(metrics->IsRunning());
|
||||
XCTAssertTrue(metrics->running_);
|
||||
|
||||
metrics->StopPoll();
|
||||
|
||||
// After stopping, the internal state is no longer marked running
|
||||
XCTAssertFalse(metrics->IsRunning());
|
||||
XCTAssertFalse(metrics->running_);
|
||||
|
||||
metrics->StopPoll();
|
||||
|
||||
// Double-stop doesn't change the running state
|
||||
XCTAssertFalse(metrics->IsRunning());
|
||||
XCTAssertFalse(metrics->running_);
|
||||
}
|
||||
|
||||
- (void)testSetInterval {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
});
|
||||
|
||||
XCTAssertEqual(100, metrics->Interval());
|
||||
XCTAssertEqual(100, metrics->interval_);
|
||||
|
||||
metrics->SetInterval(200);
|
||||
XCTAssertEqual(200, metrics->Interval());
|
||||
XCTAssertEqual(200, metrics->interval_);
|
||||
}
|
||||
|
||||
- (void)testProcessorToString {
|
||||
@@ -179,25 +186,60 @@ using santa::santad::ProcessorToString;
|
||||
XCTAssertThrows(EventDispositionToString((EventDisposition)12345));
|
||||
}
|
||||
|
||||
- (void)testFileAccessMetricStatusToString {
|
||||
std::map<FileAccessMetricStatus, NSString *> statusToString = {
|
||||
{FileAccessMetricStatus::kOK, @"OK"},
|
||||
{FileAccessMetricStatus::kBlockedUser, @"BLOCKED_USER"},
|
||||
};
|
||||
|
||||
for (const auto &kv : statusToString) {
|
||||
XCTAssertEqualObjects(FileAccessMetricStatusToString(kv.first), kv.second);
|
||||
}
|
||||
|
||||
XCTAssertThrows(FileAccessMetricStatusToString((FileAccessMetricStatus)12345));
|
||||
}
|
||||
|
||||
- (void)testFileAccessPolicyDecisionToString {
|
||||
std::map<FileAccessPolicyDecision, NSString *> decisionToString = {
|
||||
{FileAccessPolicyDecision::kDenied, @"Denied"},
|
||||
{FileAccessPolicyDecision::kDeniedInvalidSignature, @"Denied"},
|
||||
{FileAccessPolicyDecision::kDeniedInvalidSignature, @"Denied"},
|
||||
};
|
||||
|
||||
for (const auto &kv : decisionToString) {
|
||||
XCTAssertEqualObjects(FileAccessPolicyDecisionToString(kv.first), kv.second);
|
||||
}
|
||||
|
||||
std::set<FileAccessPolicyDecision> decisionToStringThrows = {
|
||||
FileAccessPolicyDecision::kNoPolicy,
|
||||
FileAccessPolicyDecision::kAllowed,
|
||||
FileAccessPolicyDecision::kAllowedReadAccess,
|
||||
(FileAccessPolicyDecision)12345,
|
||||
};
|
||||
for (const auto &v : decisionToStringThrows) {
|
||||
XCTAssertThrows(FileAccessPolicyDecisionToString(v));
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testSetEventMetrics {
|
||||
int64_t nanos = 1234;
|
||||
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
|
||||
// Initial maps are empty
|
||||
XCTAssertEqual(metrics->EventCounts().size(), 0);
|
||||
XCTAssertEqual(metrics->EventTimes().size(), 0);
|
||||
XCTAssertEqual(metrics->event_counts_cache_.size(), 0);
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 0);
|
||||
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
|
||||
EventDisposition::kProcessed, nanos);
|
||||
|
||||
// Check sizes after setting metrics once
|
||||
XCTAssertEqual(metrics->EventCounts().size(), 1);
|
||||
XCTAssertEqual(metrics->EventTimes().size(), 1);
|
||||
XCTAssertEqual(metrics->event_counts_cache_.size(), 1);
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 1);
|
||||
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
|
||||
EventDisposition::kProcessed, nanos);
|
||||
@@ -205,8 +247,8 @@ using santa::santad::ProcessorToString;
|
||||
EventDisposition::kProcessed, nanos * 2);
|
||||
|
||||
// Re-check expected counts. One was an update, so should only be 2 items
|
||||
XCTAssertEqual(metrics->EventCounts().size(), 2);
|
||||
XCTAssertEqual(metrics->EventTimes().size(), 2);
|
||||
XCTAssertEqual(metrics->event_counts_cache_.size(), 2);
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 2);
|
||||
|
||||
// Check map values
|
||||
EventCountTuple ecExec{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
|
||||
@@ -216,36 +258,72 @@ using santa::santad::ProcessorToString;
|
||||
EventTimesTuple etExec{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC};
|
||||
EventTimesTuple etOpen{Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN};
|
||||
|
||||
XCTAssertEqual(metrics->EventCounts()[ecExec], 2);
|
||||
XCTAssertEqual(metrics->EventCounts()[ecOpen], 1);
|
||||
XCTAssertEqual(metrics->EventTimes()[etExec], nanos);
|
||||
XCTAssertEqual(metrics->EventTimes()[etOpen], nanos * 2);
|
||||
XCTAssertEqual(metrics->event_counts_cache_[ecExec], 2);
|
||||
XCTAssertEqual(metrics->event_counts_cache_[ecOpen], 1);
|
||||
XCTAssertEqual(metrics->event_times_cache_[etExec], nanos);
|
||||
XCTAssertEqual(metrics->event_times_cache_[etOpen], nanos * 2);
|
||||
}
|
||||
|
||||
- (void)testSetRateLimitingMetrics {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
|
||||
// Initial map is empty
|
||||
XCTAssertEqual(metrics->RateLimitCounts().size(), 0);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 0);
|
||||
|
||||
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 123);
|
||||
|
||||
// Check sizes after setting metrics once
|
||||
XCTAssertEqual(metrics->RateLimitCounts().size(), 1);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 1);
|
||||
|
||||
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 456);
|
||||
metrics->SetRateLimitingMetrics(Processor::kAuthorizer, 789);
|
||||
|
||||
// Re-check expected counts. One was an update, so should only be 2 items
|
||||
XCTAssertEqual(metrics->RateLimitCounts().size(), 2);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 2);
|
||||
|
||||
// Check map values
|
||||
XCTAssertEqual(metrics->RateLimitCounts()[Processor::kFileAccessAuthorizer], 123 + 456);
|
||||
XCTAssertEqual(metrics->RateLimitCounts()[Processor::kAuthorizer], 789);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_[Processor::kFileAccessAuthorizer], 123 + 456);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_[Processor::kAuthorizer], 789);
|
||||
}
|
||||
|
||||
- (void)testSetFileAccessEventMetrics {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
|
||||
// Initial map is empty
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 0);
|
||||
|
||||
metrics->SetFileAccessEventMetrics("v1.0", "rule_abc", FileAccessMetricStatus::kOK,
|
||||
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied);
|
||||
|
||||
// Check sizes after setting metrics once
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 1);
|
||||
|
||||
// Update the previous metric
|
||||
metrics->SetFileAccessEventMetrics("v1.0", "rule_abc", FileAccessMetricStatus::kOK,
|
||||
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied);
|
||||
|
||||
// Add a second metric
|
||||
metrics->SetFileAccessEventMetrics("v1.0", "rule_xyz", FileAccessMetricStatus::kOK,
|
||||
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied);
|
||||
|
||||
// Re-check expected counts. One was an update, so should only be 2 items
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 2);
|
||||
|
||||
FileAccessEventCountTuple ruleAbc{"v1.0", "rule_abc", FileAccessMetricStatus::kOK,
|
||||
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied};
|
||||
FileAccessEventCountTuple ruleXyz{"v1.0", "rule_xyz", FileAccessMetricStatus::kOK,
|
||||
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied};
|
||||
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_[ruleAbc], 2);
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_[ruleXyz], 1);
|
||||
}
|
||||
|
||||
- (void)testFlushMetrics {
|
||||
@@ -266,22 +344,26 @@ using santa::santad::ProcessorToString;
|
||||
});
|
||||
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, mockEventProcessingTimes,
|
||||
mockEventCounts, mockEventCounts, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
auto metrics =
|
||||
std::make_shared<MetricsPeer>(self.q, timer, 100, mockEventProcessingTimes, mockEventCounts,
|
||||
mockEventCounts, mockEventCounts, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
|
||||
EventDisposition::kProcessed, nanos);
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN,
|
||||
EventDisposition::kProcessed, nanos * 2);
|
||||
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 123);
|
||||
metrics->SetFileAccessEventMetrics("v1.0", "rule_abc", FileAccessMetricStatus::kOK,
|
||||
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied);
|
||||
|
||||
// First ensure we have the expected map sizes
|
||||
XCTAssertEqual(metrics->EventCounts().size(), 2);
|
||||
XCTAssertEqual(metrics->EventTimes().size(), 2);
|
||||
XCTAssertEqual(metrics->RateLimitCounts().size(), 1);
|
||||
XCTAssertEqual(metrics->event_counts_cache_.size(), 2);
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 2);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 1);
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 1);
|
||||
|
||||
metrics->FlushMetrics();
|
||||
|
||||
@@ -293,11 +375,13 @@ using santa::santad::ProcessorToString;
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (3)");
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (4)");
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (5)");
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (6)");
|
||||
|
||||
// After a flush, map sizes should be reset to 0
|
||||
XCTAssertEqual(metrics->EventCounts().size(), 0);
|
||||
XCTAssertEqual(metrics->EventTimes().size(), 0);
|
||||
XCTAssertEqual(metrics->RateLimitCounts().size(), 0);
|
||||
XCTAssertEqual(metrics->event_counts_cache_.size(), 0);
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 0);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 0);
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 0);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -156,6 +156,21 @@ double watchdogRAMPeak = 0;
|
||||
reply([SNTConfigurator configurator].staticRules.count);
|
||||
}
|
||||
|
||||
- (void)retrieveAllRules:(void (^)(NSArray<SNTRule *> *, NSError *))reply {
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
|
||||
// Do not return any rules if syncBaseURL is set and return an error.
|
||||
if (config.syncBaseURL) {
|
||||
reply(@[], [NSError errorWithDomain:@"com.google.santad"
|
||||
code:403 // (TODO) define error code
|
||||
userInfo:@{NSLocalizedDescriptionKey : @"SyncBaseURL is set"}]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray<SNTRule *> *rules = [[SNTDatabaseController ruleTable] retrieveAllRules];
|
||||
reply(rules, nil);
|
||||
}
|
||||
|
||||
#pragma mark Decision Ops
|
||||
|
||||
- (void)decisionForFilePath:(NSString *)filePath
|
||||
@@ -252,6 +267,11 @@ double watchdogRAMPeak = 0;
|
||||
reply();
|
||||
}
|
||||
|
||||
- (void)setOverrideFileAccessAction:(NSString *)action reply:(void (^)(void))reply {
|
||||
[[SNTConfigurator configurator] setSyncServerOverrideFileAccessAction:action];
|
||||
reply();
|
||||
}
|
||||
|
||||
- (void)enableBundles:(void (^)(BOOL))reply {
|
||||
reply([SNTConfigurator configurator].enableBundles);
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
// Respond with the decision.
|
||||
postAction(action);
|
||||
|
||||
// Increment counters;
|
||||
// Increment metric counters
|
||||
[self incrementEventCounters:cd.decision];
|
||||
|
||||
// Log to database if necessary.
|
||||
@@ -300,13 +300,13 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
}
|
||||
|
||||
if (!cd.silentBlock) {
|
||||
// Let the user know what happened, both on the terminal and in the GUI.
|
||||
NSAttributedString *s = [SNTBlockMessage attributedBlockMessageForEvent:se
|
||||
customMessage:cd.customMsg];
|
||||
if (!config.enableSilentTTYMode && self->_ttyWriter && TTYWriter::CanWrite(targetProc)) {
|
||||
// Let the user know what happened on the terminal
|
||||
NSAttributedString *s = [SNTBlockMessage attributedBlockMessageForEvent:se
|
||||
customMessage:cd.customMsg];
|
||||
|
||||
if (targetProc->tty && targetProc->tty->path.length > 0 && !config.enableSilentTTYMode &&
|
||||
self->_ttyWriter) {
|
||||
NSMutableString *msg = [NSMutableString stringWithCapacity:1024];
|
||||
// Escape sequences `\033[1m` and `\033[0m` begin/end bold lettering
|
||||
[msg appendFormat:@"\n\033[1mSanta\033[0m\n\n%@\n\n", s.string];
|
||||
[msg appendFormat:@"\033[1mPath: \033[0m %@\n"
|
||||
@"\033[1mIdentifier:\033[0m %@\n"
|
||||
@@ -317,9 +317,10 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
[msg appendFormat:@"More info:\n%@\n\n", detailURL.absoluteString];
|
||||
}
|
||||
|
||||
self->_ttyWriter->Write(targetProc->tty->path.data, msg);
|
||||
self->_ttyWriter->Write(targetProc, msg);
|
||||
}
|
||||
|
||||
// Let the user know what happened in the GUI.
|
||||
[self.notifierQueue addEvent:se withCustomMessage:cd.customMsg andCustomURL:cd.customURL];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,6 +464,80 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
[self checkMetricCounters:kAllowTransitive expected:@0];
|
||||
}
|
||||
|
||||
- (void)testSigningIDAllowCompilerRule {
|
||||
OCMStub([self.mockFileInfo isMachO]).andReturn(YES);
|
||||
OCMStub([self.mockFileInfo SHA256]).andReturn(@"a");
|
||||
|
||||
OCMStub([self.mockConfigurator enableTransitiveRules]).andReturn(YES);
|
||||
|
||||
NSString *signingID = [NSString stringWithFormat:@"%s:%s", kExampleTeamID, kExampleSigningID];
|
||||
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowCompiler;
|
||||
rule.type = SNTRuleTypeSigningID;
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:signingID
|
||||
certificateSHA256:nil
|
||||
teamID:@(kExampleTeamID)])
|
||||
.andReturn(rule);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllowCompiler
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kExampleTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kExampleSigningID);
|
||||
}];
|
||||
|
||||
[self checkMetricCounters:kAllowCompiler expected:@1];
|
||||
}
|
||||
|
||||
- (void)testSigningIDAllowTransitiveRuleDisabled {
|
||||
OCMStub([self.mockFileInfo isMachO]).andReturn(YES);
|
||||
OCMStub([self.mockFileInfo SHA256]).andReturn(@"a");
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
OCMStub([self.mockConfigurator enableTransitiveRules]).andReturn(NO);
|
||||
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowTransitive;
|
||||
rule.type = SNTRuleTypeSigningID;
|
||||
|
||||
NSString *signingID = [NSString stringWithFormat:@"%s:%s", kExampleTeamID, kExampleSigningID];
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:signingID
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->signing_id = MakeESStringToken("com.google.santa.test");
|
||||
}];
|
||||
|
||||
OCMVerifyAllWithDelay(self.mockEventDatabase, 1);
|
||||
[self checkMetricCounters:kAllowSigningID expected:@0];
|
||||
[self checkMetricCounters:kAllowTransitive expected:@0];
|
||||
}
|
||||
|
||||
- (void)testThatPlatformBinaryCachedDecisionsSetModeCorrectly {
|
||||
OCMStub([self.mockFileInfo isMachO]).andReturn(YES);
|
||||
OCMStub([self.mockFileInfo SHA256]).andReturn(@"a");
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
OCMStub([self.mockConfigurator enableTransitiveRules]).andReturn(NO);
|
||||
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.decision = SNTEventStateAllowBinary;
|
||||
OCMStub([self.mockRuleDatabase criticalSystemBinaries]).andReturn(@{@"a" : cd});
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowBinary expected:@1];
|
||||
[self checkMetricCounters:kAllowUnknown expected:@0];
|
||||
|
||||
XCTAssertEqual(cd.decisionClientMode, SNTClientModeLockdown);
|
||||
}
|
||||
|
||||
- (void)testDefaultDecision {
|
||||
OCMStub([self.mockFileInfo isMachO]).andReturn(YES);
|
||||
OCMStub([self.mockFileInfo SHA256]).andReturn(@"a");
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
@interface SNTPolicyProcessor ()
|
||||
@property SNTRuleTable *ruleTable;
|
||||
@property SNTConfigurator *configurator;
|
||||
@end
|
||||
|
||||
@implementation SNTPolicyProcessor
|
||||
@@ -35,6 +36,7 @@
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_ruleTable = ruleTable;
|
||||
_configurator = [SNTConfigurator configurator];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -49,10 +51,16 @@
|
||||
cd.teamID = teamID;
|
||||
cd.signingID = signingID;
|
||||
|
||||
SNTClientMode mode = [self.configurator clientMode];
|
||||
cd.decisionClientMode = mode;
|
||||
|
||||
// If the binary is a critical system binary, don't check its signature.
|
||||
// The binary was validated at startup when the rule table was initialized.
|
||||
SNTCachedDecision *systemCd = self.ruleTable.criticalSystemBinaries[cd.sha256];
|
||||
if (systemCd) return systemCd;
|
||||
if (systemCd) {
|
||||
systemCd.decisionClientMode = mode;
|
||||
return systemCd;
|
||||
}
|
||||
|
||||
NSError *csInfoError;
|
||||
if (certificateSHA256.length) {
|
||||
@@ -74,27 +82,24 @@
|
||||
cd.teamID = teamID
|
||||
?: [csInfo.signingInformation
|
||||
objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier];
|
||||
teamID = cd.teamID;
|
||||
|
||||
// Ensure that if no teamID exists that the signing info confirms it is a
|
||||
// platform binary. If not, remove the signingID.
|
||||
if (!teamID && signingID) {
|
||||
if (!cd.teamID && cd.signingID) {
|
||||
id platformID = [csInfo.signingInformation
|
||||
objectForKey:(__bridge NSString *)kSecCodeInfoPlatformIdentifier];
|
||||
if (![platformID isKindOfClass:[NSNumber class]] || [platformID intValue] == 0) {
|
||||
signingID = nil;
|
||||
cd.signingID = nil;
|
||||
}
|
||||
}
|
||||
|
||||
cd.signingID = signingID;
|
||||
}
|
||||
}
|
||||
cd.quarantineURL = fileInfo.quarantineDataURL;
|
||||
|
||||
SNTRule *rule = [self.ruleTable ruleForBinarySHA256:cd.sha256
|
||||
signingID:signingID
|
||||
signingID:cd.signingID
|
||||
certificateSHA256:cd.certSHA256
|
||||
teamID:teamID];
|
||||
teamID:cd.teamID];
|
||||
if (rule) {
|
||||
switch (rule.type) {
|
||||
case SNTRuleTypeBinary:
|
||||
@@ -110,7 +115,7 @@
|
||||
// If transitive rules are enabled, then SNTRuleStateAllowListCompiler rules
|
||||
// become SNTEventStateAllowCompiler decisions. Otherwise we treat the rule as if
|
||||
// it were SNTRuleStateAllow.
|
||||
if ([[SNTConfigurator configurator] enableTransitiveRules]) {
|
||||
if ([self.configurator enableTransitiveRules]) {
|
||||
cd.decision = SNTEventStateAllowCompiler;
|
||||
} else {
|
||||
cd.decision = SNTEventStateAllowBinary;
|
||||
@@ -120,7 +125,7 @@
|
||||
// If transitive rules are enabled, then SNTRuleStateAllowTransitive
|
||||
// rules become SNTEventStateAllowTransitive decisions. Otherwise, we treat the
|
||||
// rule as if it were SNTRuleStateUnknown.
|
||||
if ([[SNTConfigurator configurator] enableTransitiveRules]) {
|
||||
if ([self.configurator enableTransitiveRules]) {
|
||||
cd.decision = SNTEventStateAllowTransitive;
|
||||
return cd;
|
||||
} else {
|
||||
@@ -132,6 +137,16 @@
|
||||
case SNTRuleTypeSigningID:
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowSigningID; return cd;
|
||||
case SNTRuleStateAllowCompiler:
|
||||
// If transitive rules are enabled, then SNTRuleStateAllowListCompiler rules
|
||||
// become SNTEventStateAllowCompiler decisions. Otherwise we treat the rule as if
|
||||
// it were SNTRuleStateAllowSigningID.
|
||||
if ([self.configurator enableTransitiveRules]) {
|
||||
cd.decision = SNTEventStateAllowCompiler;
|
||||
} else {
|
||||
cd.decision = SNTEventStateAllowSigningID;
|
||||
}
|
||||
return cd;
|
||||
case SNTRuleStateSilentBlock:
|
||||
cd.silentBlock = YES;
|
||||
// intentional fallthrough
|
||||
@@ -198,9 +213,6 @@
|
||||
return cd;
|
||||
}
|
||||
|
||||
SNTClientMode mode = [[SNTConfigurator configurator] clientMode];
|
||||
cd.decisionClientMode = mode;
|
||||
|
||||
switch (mode) {
|
||||
case SNTClientModeMonitor: cd.decision = SNTEventStateAllowUnknown; return cd;
|
||||
case SNTClientModeLockdown: cd.decision = SNTEventStateBlockUnknown; return cd;
|
||||
|
||||
@@ -103,10 +103,11 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
|
||||
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:esapi
|
||||
metrics:metrics
|
||||
logger:logger
|
||||
authResultCache:auth_result_cache];
|
||||
authResultCache:auth_result_cache
|
||||
blockUSBMount:[configurator blockUSBMount]
|
||||
remountUSBMode:[configurator remountUSBMode]
|
||||
startupPreferences:[configurator onStartUSBOptions]];
|
||||
|
||||
device_client.blockUSBMount = [configurator blockUSBMount];
|
||||
device_client.remountArgs = [configurator remountUSBMode];
|
||||
device_client.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
[[notifier_queue.notifierConnection remoteObjectProxy]
|
||||
postUSBBlockNotification:event
|
||||
@@ -145,11 +146,14 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
|
||||
ttyWriter:tty_writer];
|
||||
watch_items->RegisterClient(access_authorizer_client);
|
||||
|
||||
access_authorizer_client.fileAccessBlockCallback = ^(SNTFileAccessEvent *event) {
|
||||
[[notifier_queue.notifierConnection remoteObjectProxy]
|
||||
postFileAccessBlockNotification:event
|
||||
withCustomMessage:@"Access to the resource has been denied!"];
|
||||
};
|
||||
access_authorizer_client.fileAccessBlockCallback =
|
||||
^(SNTFileAccessEvent *event, NSString *customMsg, NSString *customURL, NSString *customText) {
|
||||
[[notifier_queue.notifierConnection remoteObjectProxy]
|
||||
postFileAccessBlockNotification:event
|
||||
customMessage:customMsg
|
||||
customURL:customURL
|
||||
customText:customText];
|
||||
};
|
||||
}
|
||||
|
||||
EstablishSyncServiceConnection(syncd_queue);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
@@ -31,6 +32,7 @@
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h"
|
||||
#import "Source/santad/Metrics.h"
|
||||
#import "Source/santad/SNTDatabaseController.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
#include "Source/santad/SantadDeps.h"
|
||||
|
||||
using santa::santad::SantadDeps;
|
||||
@@ -41,7 +43,7 @@ static const char *kAllowedSigningID = "com.google.allowed_signing_id";
|
||||
static const char *kBlockedSigningID = "com.google.blocked_signing_id";
|
||||
static const char *kNoRuleMatchSigningID = "com.google.no_rule_match_signing_id";
|
||||
static const char *kBlockedTeamID = "EQHXZ8M8AV";
|
||||
static const char *kNoRuleMatchTeamID = "ABC1234XYZ";
|
||||
static const char *kAllowedTeamID = "TJNVEKW352";
|
||||
|
||||
@interface SantadTest : XCTestCase
|
||||
@property id mockSNTDatabaseController;
|
||||
@@ -62,10 +64,17 @@ static const char *kNoRuleMatchTeamID = "ABC1234XYZ";
|
||||
- (BOOL)checkBinaryExecution:(NSString *)binaryName
|
||||
wantResult:(es_auth_result_t)wantResult
|
||||
clientMode:(NSInteger)clientMode
|
||||
cdValidator:(BOOL (^)(SNTCachedDecision *))cdValidator
|
||||
messageSetup:(void (^)(es_message_t *))messageSetupBlock {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
|
||||
id mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
|
||||
OCMStub([mockDecisionCache sharedCache]).andReturn(mockDecisionCache);
|
||||
if (cdValidator) {
|
||||
OCMExpect([mockDecisionCache cacheDecision:[OCMArg checkWithBlock:cdValidator]]);
|
||||
}
|
||||
|
||||
id mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
|
||||
OCMStub([mockConfigurator configurator]).andReturn(mockConfigurator);
|
||||
@@ -150,6 +159,8 @@ static const char *kNoRuleMatchTeamID = "ABC1234XYZ";
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:10.0];
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockDecisionCache), "Unable to verify SNTCachedDecision properties");
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
|
||||
"Failed waiting for message to be processed...");
|
||||
@@ -159,10 +170,12 @@ static const char *kNoRuleMatchTeamID = "ABC1234XYZ";
|
||||
|
||||
- (BOOL)checkBinaryExecution:(NSString *)binaryName
|
||||
wantResult:(es_auth_result_t)wantResult
|
||||
clientMode:(NSInteger)clientMode {
|
||||
clientMode:(NSInteger)clientMode
|
||||
cdValidator:(BOOL (^)(SNTCachedDecision *))cdValidator {
|
||||
return [self checkBinaryExecution:binaryName
|
||||
wantResult:wantResult
|
||||
clientMode:clientMode
|
||||
cdValidator:cdValidator
|
||||
messageSetup:nil];
|
||||
}
|
||||
|
||||
@@ -174,145 +187,221 @@ static const char *kNoRuleMatchTeamID = "ABC1234XYZ";
|
||||
- (void)testBinaryWithSHA256BlockRuleIsBlockedInLockdownMode {
|
||||
[self checkBinaryExecution:@"badbinary"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeLockdown];
|
||||
clientMode:SNTClientModeLockdown
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateBlockBinary;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSHA256BlockRuleIsBlockedInMonitorMode {
|
||||
[self checkBinaryExecution:@"badbinary"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeMonitor];
|
||||
clientMode:SNTClientModeMonitor
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateBlockBinary;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSHA256AllowRuleIsNotBlockedInLockdownMode {
|
||||
[self checkBinaryExecution:@"goodbinary"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeLockdown];
|
||||
clientMode:SNTClientModeLockdown
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateAllowBinary;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSHA256AllowRuleIsNotBlockedInMonitorMode {
|
||||
[self checkBinaryExecution:@"goodbinary"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor];
|
||||
clientMode:SNTClientModeMonitor
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateAllowBinary;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithCertificateAllowRuleIsNotBlockedInLockdownMode {
|
||||
[self checkBinaryExecution:@"goodcert"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeLockdown];
|
||||
clientMode:SNTClientModeLockdown
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateAllowCertificate;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithCertificateAllowRuleIsNotBlockedInMonitorMode {
|
||||
[self checkBinaryExecution:@"goodcert"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor];
|
||||
clientMode:SNTClientModeMonitor
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateAllowCertificate;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithCertificateBlockRuleIsBlockedInLockdownMode {
|
||||
[self checkBinaryExecution:@"badcert"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeLockdown];
|
||||
clientMode:SNTClientModeLockdown
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateBlockCertificate;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithCertificateBlockRuleIsNotBlockedInMonitorMode {
|
||||
- (void)testBinaryWithCertificateBlockRuleIsBlockedInMonitorMode {
|
||||
[self checkBinaryExecution:@"badcert"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeMonitor];
|
||||
clientMode:SNTClientModeMonitor
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateBlockCertificate;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithTeamIDBlockRuleIsBlockedInLockdownMode {
|
||||
- (void)testBinaryWithTeamIDAllowRuleAndNoSigningIDMatchIsAllowedInLockdownMode {
|
||||
[self checkBinaryExecution:@"allowed_teamid"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeLockdown
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateAllowTeamID;
|
||||
}
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kAllowedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithTeamIDAllowRuleAndNoSigningIDMatchIsAllowedInMonitorMode {
|
||||
[self checkBinaryExecution:@"allowed_teamid"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateAllowTeamID;
|
||||
}
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kAllowedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithTeamIDBlockRuleAndNoSigningIDMatchIsBlockedInLockdownMode {
|
||||
[self checkBinaryExecution:@"banned_teamid"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeLockdown];
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeLockdown
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateBlockTeamID;
|
||||
}
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithTeamIDBlockRuleIsBlockedInMonitorMode {
|
||||
- (void)testBinaryWithTeamIDBlockRuleAndNoSigningIDMatchIsBlockedInMonitorMode {
|
||||
[self checkBinaryExecution:@"banned_teamid"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeMonitor];
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeMonitor
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateBlockTeamID;
|
||||
}
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSigningIDBlockRuleAndCertAllowedRuleIsBlockedInMonitorMode {
|
||||
[self checkBinaryExecution:@"cert_hash_allowed_signingid_blocked"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeMonitor
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kBlockedSigningID);
|
||||
}];
|
||||
- (void)testBinaryWithSigningIDBlockRuleIsBlockedInLockdownMode {
|
||||
[self checkBinaryExecution:@"banned_signingid"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeLockdown
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateBlockSigningID;
|
||||
}
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kBlockedSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSigningIDNoRuleMatchAndCertAllowedRuleIsAllowedInMonitorMode {
|
||||
[self checkBinaryExecution:@"cert_hash_allowed_signingid_not_matched"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
|
||||
}];
|
||||
- (void)testBinaryWithSigningIDBlockRuleIsBlockedInMonitorMode {
|
||||
[self checkBinaryExecution:@"banned_signingid"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeMonitor
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateBlockSigningID;
|
||||
}
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kBlockedSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSigningIDBlockRuleMatchAndCertAllowedRuleIsAllowedInMonitorMode {
|
||||
[self checkBinaryExecution:@"binary_hash_allowed_signingid_blocked"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kBlockedSigningID);
|
||||
}];
|
||||
- (void)testBinaryWithSigningIDAllowRuleIsAllowedInMonitorMode {
|
||||
[self checkBinaryExecution:@"allowed_signingid"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateAllowSigningID;
|
||||
}
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kAllowedSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSigningIDNoRuleMatchIsAllowedInMonitorMode {
|
||||
[self checkBinaryExecution:@"noop"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kNoRuleMatchTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSigningIDNoRuleMatchIsBlockedInLockdownMode {
|
||||
[self checkBinaryExecution:@"noop"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeLockdown
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kNoRuleMatchTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithAllowedSigningIDRuleIsAllowedInLockdownMode {
|
||||
[self checkBinaryExecution:@"noop"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeLockdown
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kNoRuleMatchTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kAllowedSigningID);
|
||||
}];
|
||||
- (void)testBinaryWithSigningIDAllowRuleIsAllowedInLockdownMode {
|
||||
[self checkBinaryExecution:@"allowed_signingid"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateAllowSigningID;
|
||||
}
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kAllowedSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSHA256AllowRuleAndBlockedTeamIDRuleIsAllowedInLockdownMode {
|
||||
[self checkBinaryExecution:@"banned_teamid_allowed_binary"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeLockdown];
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeLockdown
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateAllowBinary;
|
||||
}
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSHA256AllowRuleAndBlockedTeamIDRuleIsAllowedInMonitorMode {
|
||||
[self checkBinaryExecution:@"banned_teamid_allowed_binary"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor];
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateAllowBinary;
|
||||
}
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithoutBlockOrAllowRuleIsAllowedInLockdownMode {
|
||||
- (void)testBinaryWithoutBlockOrAllowRuleIsBlockedInLockdownMode {
|
||||
[self checkBinaryExecution:@"noop"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeLockdown];
|
||||
clientMode:SNTClientModeLockdown
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateBlockUnknown;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithoutBlockOrAllowRuleIsAllowedInMonitorMode {
|
||||
[self checkBinaryExecution:@"noop"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor];
|
||||
clientMode:SNTClientModeMonitor
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateAllowUnknown;
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#ifndef SANTA__SANTAD__TTYWRITER_H
|
||||
#define SANTA__SANTAD__TTYWRITER_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
@@ -37,7 +38,9 @@ class TTYWriter {
|
||||
TTYWriter(const TTYWriter &other) = delete;
|
||||
TTYWriter &operator=(const TTYWriter &other) = delete;
|
||||
|
||||
void Write(const char *tty, NSString *msg);
|
||||
static bool CanWrite(const es_process_t *proc);
|
||||
|
||||
void Write(const es_process_t *proc, NSString *msg);
|
||||
|
||||
private:
|
||||
dispatch_queue_t q_;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include <string.h>
|
||||
#include <sys/errno.h>
|
||||
#include <sys/param.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/common/String.h"
|
||||
@@ -24,7 +25,7 @@ namespace santa::santad {
|
||||
|
||||
std::unique_ptr<TTYWriter> TTYWriter::Create() {
|
||||
dispatch_queue_t q = dispatch_queue_create_with_target(
|
||||
"com.google.santa.ttywriter", DISPATCH_QUEUE_SERIAL,
|
||||
"com.google.santa.ttywriter", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
|
||||
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
|
||||
|
||||
if (!q) {
|
||||
@@ -36,20 +37,30 @@ std::unique_ptr<TTYWriter> TTYWriter::Create() {
|
||||
|
||||
TTYWriter::TTYWriter(dispatch_queue_t q) : q_(q) {}
|
||||
|
||||
void TTYWriter::Write(const char *tty, NSString *msg) {
|
||||
bool TTYWriter::CanWrite(const es_process_t *proc) {
|
||||
return proc && proc->tty && proc->tty->path.length > 0;
|
||||
}
|
||||
|
||||
void TTYWriter::Write(const es_process_t *proc, NSString *msg) {
|
||||
if (!CanWrite(proc)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy the data from the es_process_t so the ES message doesn't
|
||||
// need to be retained
|
||||
NSString *tty = santa::common::StringToNSString(proc->tty->path.data);
|
||||
|
||||
dispatch_async(q_, ^{
|
||||
@autoreleasepool {
|
||||
int fd = open(tty, O_WRONLY | O_NOCTTY);
|
||||
if (fd == -1) {
|
||||
LOGW(@"Failed to open TTY for writing: %s", strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
std::string_view str = santa::common::NSStringToUTF8StringView(msg);
|
||||
write(fd, str.data(), str.length());
|
||||
|
||||
close(fd);
|
||||
int fd = open(tty.UTF8String, O_WRONLY | O_NOCTTY);
|
||||
if (fd == -1) {
|
||||
LOGW(@"Failed to open TTY for writing: %s", strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
std::string_view str = santa::common::NSStringToUTF8StringView(msg);
|
||||
write(fd, str.data(), str.length());
|
||||
|
||||
close(fd);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
BIN
Source/santad/testdata/binaryrules/allowed_signingid
vendored
Executable file
BIN
Source/santad/testdata/binaryrules/allowed_signingid
vendored
Executable file
Binary file not shown.
BIN
Source/santad/testdata/binaryrules/allowed_teamid
vendored
Normal file
BIN
Source/santad/testdata/binaryrules/allowed_teamid
vendored
Normal file
Binary file not shown.
BIN
Source/santad/testdata/binaryrules/banned_signingid
vendored
Executable file
BIN
Source/santad/testdata/binaryrules/banned_signingid
vendored
Executable file
Binary file not shown.
Binary file not shown.
BIN
Source/santad/testdata/binaryrules/rules.db
vendored
BIN
Source/santad/testdata/binaryrules/rules.db
vendored
Binary file not shown.
35
Source/santad/testdata/protobuf/v1/cs_invalidated.json
vendored
Normal file
35
Source/santad/testdata/protobuf/v1/cs_invalidated.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"instigator": {
|
||||
"id": {
|
||||
"pid": 12,
|
||||
"pidversion": 34
|
||||
},
|
||||
"parent_id": {
|
||||
"pid": 56,
|
||||
"pidversion": 78
|
||||
},
|
||||
"original_parent_pid": 56,
|
||||
"group_id": 111,
|
||||
"session_id": 222,
|
||||
"effective_user": {
|
||||
"uid": -2,
|
||||
"name": "nobody"
|
||||
},
|
||||
"effective_group": {
|
||||
"gid": -1,
|
||||
"name": "nogroup"
|
||||
},
|
||||
"real_user": {
|
||||
"uid": -2,
|
||||
"name": "nobody"
|
||||
},
|
||||
"real_group": {
|
||||
"gid": -1,
|
||||
"name": "nogroup"
|
||||
},
|
||||
"executable": {
|
||||
"path": "foo",
|
||||
"truncated": false
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Source/santad/testdata/protobuf/v2/cs_invalidated.json
vendored
Normal file
35
Source/santad/testdata/protobuf/v2/cs_invalidated.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"instigator": {
|
||||
"id": {
|
||||
"pid": 12,
|
||||
"pidversion": 34
|
||||
},
|
||||
"parent_id": {
|
||||
"pid": 56,
|
||||
"pidversion": 78
|
||||
},
|
||||
"original_parent_pid": 56,
|
||||
"group_id": 111,
|
||||
"session_id": 222,
|
||||
"effective_user": {
|
||||
"uid": -2,
|
||||
"name": "nobody"
|
||||
},
|
||||
"effective_group": {
|
||||
"gid": -1,
|
||||
"name": "nogroup"
|
||||
},
|
||||
"real_user": {
|
||||
"uid": -2,
|
||||
"name": "nobody"
|
||||
},
|
||||
"real_group": {
|
||||
"gid": -1,
|
||||
"name": "nogroup"
|
||||
},
|
||||
"executable": {
|
||||
"path": "foo",
|
||||
"truncated": false
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Source/santad/testdata/protobuf/v4/cs_invalidated.json
vendored
Normal file
35
Source/santad/testdata/protobuf/v4/cs_invalidated.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"instigator": {
|
||||
"id": {
|
||||
"pid": 12,
|
||||
"pidversion": 34
|
||||
},
|
||||
"parent_id": {
|
||||
"pid": 56,
|
||||
"pidversion": 78
|
||||
},
|
||||
"original_parent_pid": 56,
|
||||
"group_id": 111,
|
||||
"session_id": 222,
|
||||
"effective_user": {
|
||||
"uid": -2,
|
||||
"name": "nobody"
|
||||
},
|
||||
"effective_group": {
|
||||
"gid": -1,
|
||||
"name": "nogroup"
|
||||
},
|
||||
"real_user": {
|
||||
"uid": -2,
|
||||
"name": "nobody"
|
||||
},
|
||||
"real_group": {
|
||||
"gid": -1,
|
||||
"name": "nogroup"
|
||||
},
|
||||
"executable": {
|
||||
"path": "foo",
|
||||
"truncated": false
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Source/santad/testdata/protobuf/v5/cs_invalidated.json
vendored
Normal file
35
Source/santad/testdata/protobuf/v5/cs_invalidated.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"instigator": {
|
||||
"id": {
|
||||
"pid": 12,
|
||||
"pidversion": 34
|
||||
},
|
||||
"parent_id": {
|
||||
"pid": 56,
|
||||
"pidversion": 78
|
||||
},
|
||||
"original_parent_pid": 56,
|
||||
"group_id": 111,
|
||||
"session_id": 222,
|
||||
"effective_user": {
|
||||
"uid": -2,
|
||||
"name": "nobody"
|
||||
},
|
||||
"effective_group": {
|
||||
"gid": -1,
|
||||
"name": "nogroup"
|
||||
},
|
||||
"real_user": {
|
||||
"uid": -2,
|
||||
"name": "nobody"
|
||||
},
|
||||
"real_group": {
|
||||
"gid": -1,
|
||||
"name": "nogroup"
|
||||
},
|
||||
"executable": {
|
||||
"path": "foo",
|
||||
"truncated": false
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Source/santad/testdata/protobuf/v6/cs_invalidated.json
vendored
Normal file
35
Source/santad/testdata/protobuf/v6/cs_invalidated.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"instigator": {
|
||||
"id": {
|
||||
"pid": 12,
|
||||
"pidversion": 34
|
||||
},
|
||||
"parent_id": {
|
||||
"pid": 56,
|
||||
"pidversion": 78
|
||||
},
|
||||
"original_parent_pid": 56,
|
||||
"group_id": 111,
|
||||
"session_id": 222,
|
||||
"effective_user": {
|
||||
"uid": -2,
|
||||
"name": "nobody"
|
||||
},
|
||||
"effective_group": {
|
||||
"gid": -1,
|
||||
"name": "nogroup"
|
||||
},
|
||||
"real_user": {
|
||||
"uid": -2,
|
||||
"name": "nobody"
|
||||
},
|
||||
"real_group": {
|
||||
"gid": -1,
|
||||
"name": "nogroup"
|
||||
},
|
||||
"executable": {
|
||||
"path": "foo",
|
||||
"truncated": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,10 @@
|
||||
}
|
||||
|
||||
- (BOOL)sync {
|
||||
[self performRequest:[self requestWithDictionary:nil]];
|
||||
[self performRequest:[self requestWithDictionary:@{
|
||||
kPostflightRulesReceived : @(self.syncState.rulesReceived),
|
||||
kPostflightRulesProcessed : @(self.syncState.rulesProcessed),
|
||||
}]];
|
||||
|
||||
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
|
||||
|
||||
@@ -93,6 +96,12 @@
|
||||
}];
|
||||
}
|
||||
|
||||
if (self.syncState.overrideFileAccessAction) {
|
||||
[rop setOverrideFileAccessAction:self.syncState.overrideFileAccessAction
|
||||
reply:^{
|
||||
}];
|
||||
}
|
||||
|
||||
// Update last sync success
|
||||
[rop setFullSyncLastSuccess:[NSDate date]
|
||||
reply:^{
|
||||
|
||||
@@ -134,6 +134,9 @@ static id EnsureType(id val, Class c) {
|
||||
self.syncState.blockUSBMount = EnsureType(resp[kBlockUSBMount], [NSNumber class]);
|
||||
self.syncState.remountUSBMode = EnsureType(resp[kRemountUSBMode], [NSArray class]);
|
||||
|
||||
self.syncState.overrideFileAccessAction =
|
||||
EnsureType(resp[kOverrideFileAccessAction], [NSString class]);
|
||||
|
||||
if ([EnsureType(resp[kCleanSync], [NSNumber class]) boolValue]) {
|
||||
SLOGD(@"Clean sync requested by server");
|
||||
self.syncState.cleanSync = YES;
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
// Note that rules from the server are filtered. We only keep those whose rule_type
|
||||
// is either BINARY or CERTIFICATE. PACKAGE rules are dropped.
|
||||
- (NSArray<SNTRule *> *)downloadNewRulesFromServer {
|
||||
self.syncState.rulesReceived = 0;
|
||||
NSMutableArray<SNTRule *> *newRules = [NSMutableArray array];
|
||||
NSString *cursor = nil;
|
||||
do {
|
||||
@@ -90,18 +91,24 @@
|
||||
return nil;
|
||||
}
|
||||
|
||||
uint32_t count = 0;
|
||||
for (NSDictionary *ruleDict in response[kRules]) {
|
||||
NSArray<NSDictionary *> *rules = response[kRules];
|
||||
|
||||
for (NSDictionary *ruleDict in rules) {
|
||||
SNTRule *rule = [[SNTRule alloc] initWithDictionary:ruleDict];
|
||||
if (rule) {
|
||||
[self processBundleNotificationsForRule:rule fromDictionary:ruleDict];
|
||||
[newRules addObject:rule];
|
||||
count++;
|
||||
if (!rule) {
|
||||
SLOGD(@"Ignoring bad rule: %@", ruleDict);
|
||||
continue;
|
||||
}
|
||||
[self processBundleNotificationsForRule:rule fromDictionary:ruleDict];
|
||||
[newRules addObject:rule];
|
||||
}
|
||||
SLOGI(@"Received %u rules", count);
|
||||
SLOGI(@"Received %lu rules", (unsigned long)rules.count);
|
||||
cursor = response[kCursor];
|
||||
self.syncState.rulesReceived += rules.count;
|
||||
} while (cursor);
|
||||
|
||||
self.syncState.rulesProcessed = newRules.count;
|
||||
|
||||
return newRules;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
@property NSNumber *blockUSBMount;
|
||||
// Array of mount args for the forced remounting feature.
|
||||
@property NSArray *remountUSBMode;
|
||||
@property NSString *overrideFileAccessAction;
|
||||
|
||||
/// Clean sync flag, if True, all existing rules should be deleted before inserting any new rules.
|
||||
@property BOOL cleanSync;
|
||||
@@ -81,4 +82,8 @@
|
||||
/// The content-encoding to use for the client uploads during the sync session.
|
||||
@property SNTSyncContentEncoding contentEncoding;
|
||||
|
||||
/// Counts of rules received and processed during rule download.
|
||||
@property NSUInteger rulesReceived;
|
||||
@property NSUInteger rulesProcessed;
|
||||
|
||||
@end
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
@@ -281,6 +282,7 @@
|
||||
XCTAssertEqual(self.syncState.eventBatchSize, 100);
|
||||
XCTAssertNil(self.syncState.allowlistRegex);
|
||||
XCTAssertNil(self.syncState.blocklistRegex);
|
||||
XCTAssertNil(self.syncState.overrideFileAccessAction);
|
||||
}
|
||||
|
||||
- (void)testPreflightTurnOnBlockUSBMount {
|
||||
@@ -318,6 +320,32 @@
|
||||
XCTAssertNil(self.syncState.blockUSBMount);
|
||||
}
|
||||
|
||||
- (void)testPreflightOverrideFileAccessAction {
|
||||
[self setupDefaultDaemonConnResponses];
|
||||
SNTSyncPreflight *sut = [[SNTSyncPreflight alloc] initWithState:self.syncState];
|
||||
|
||||
NSData *respData = [@"{\"override_file_access_action\": \"AuditOnly\", \"client_mode\": "
|
||||
@"\"LOCKDOWN\", \"batch_size\": 100}" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
[self stubRequestBody:respData response:nil error:nil validateBlock:nil];
|
||||
|
||||
XCTAssertTrue([sut sync]);
|
||||
XCTAssertEqualObjects(self.syncState.overrideFileAccessAction, @"AuditOnly");
|
||||
}
|
||||
|
||||
- (void)testPreflightOverrideFileAccessActionAbsent {
|
||||
[self setupDefaultDaemonConnResponses];
|
||||
SNTSyncPreflight *sut = [[SNTSyncPreflight alloc] initWithState:self.syncState];
|
||||
|
||||
NSData *respData = [@"{\"client_mode\": \"LOCKDOWN\", \"batch_size\": 100}"
|
||||
dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
[self stubRequestBody:respData response:nil error:nil validateBlock:nil];
|
||||
|
||||
XCTAssertTrue([sut sync]);
|
||||
XCTAssertNil(self.syncState.overrideFileAccessAction);
|
||||
}
|
||||
|
||||
- (void)testPreflightDatabaseCounts {
|
||||
SNTSyncPreflight *sut = [[SNTSyncPreflight alloc] initWithState:self.syncState];
|
||||
|
||||
@@ -603,6 +631,10 @@
|
||||
self.syncState.blockUSBMount = @0;
|
||||
XCTAssertTrue([sut sync]);
|
||||
OCMVerify([self.daemonConnRop setBlockUSBMount:NO reply:OCMOCK_ANY]);
|
||||
|
||||
self.syncState.overrideFileAccessAction = @"Disable";
|
||||
XCTAssertTrue([sut sync]);
|
||||
OCMVerify([self.daemonConnRop setOverrideFileAccessAction:@"Disable" reply:OCMOCK_ANY]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user