mirror of
https://github.com/google/santa.git
synced 2026-01-16 17:57:54 -05:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
|
||||
@@ -140,7 +140,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,12 @@ typedef NS_ENUM(NSInteger, SNTMetricFormatType) {
|
||||
SNTMetricFormatTypeMonarchJSON,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTOverrideFileAccessAction) {
|
||||
SNTOverrideFileAccessActionNone,
|
||||
SNTOverrideFileAccessActionAuditOnly,
|
||||
SNTOverrideFileAccessActionDiable,
|
||||
};
|
||||
|
||||
#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,25 @@
|
||||
///
|
||||
@property(nonatomic) NSArray<NSString *> *remountUSBMode;
|
||||
|
||||
///
|
||||
/// 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";
|
||||
@@ -129,6 +129,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 +166,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kRuleSyncLastSuccess : date,
|
||||
kSyncCleanRequired : number,
|
||||
kEnableAllEventUploadKey : number,
|
||||
kOverrideFileAccessActionKey : string,
|
||||
};
|
||||
_forcedConfigKeyTypes = @{
|
||||
kClientModeKey : number,
|
||||
@@ -219,6 +221,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kSpoolDirectoryEventMaxFlushTimeSec : number,
|
||||
kFileAccessPolicy : dictionary,
|
||||
kFileAccessPolicyPlist : string,
|
||||
kFileAccessBlockMessage : string,
|
||||
kFileAccessPolicyUpdateIntervalSec : number,
|
||||
kEnableMachineIDDecoration : number,
|
||||
kEnableForkAndExitLogging : number,
|
||||
@@ -234,6 +237,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kMetricExtraLabels : dictionary,
|
||||
kEnableAllEventUploadKey : number,
|
||||
kDisableUnknownEventUploadKey : number,
|
||||
kOverrideFileAccessActionKey : string,
|
||||
};
|
||||
_defaults = [NSUserDefaults standardUserDefaults];
|
||||
[_defaults addSuiteNamed:@"com.google.santa"];
|
||||
@@ -441,6 +445,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFileAccessBlockMessage {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicyUpdateIntervalSec {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -513,6 +521,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingOverrideFileAccessActionKey {
|
||||
return [self syncAndConfigStateSet];
|
||||
}
|
||||
|
||||
#pragma mark Public Interface
|
||||
|
||||
- (SNTClientMode)clientMode {
|
||||
@@ -861,6 +873,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 +957,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 +1116,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) \
|
||||
|
||||
@@ -429,6 +429,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
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -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: import rules from a JSON file\n"
|
||||
@" --export: export rules to a JSON file\n"
|
||||
@"\n"
|
||||
@" One of:\n"
|
||||
@" --path {path}: path of binary/bundle to add/remove.\n"
|
||||
@@ -62,6 +64,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
@" the rule state of a file.\n"
|
||||
@" --identifier {sha256|teamID|signingID}: identifier to add/remove/check\n"
|
||||
@" --sha256 {sha256}: hash to add/remove/check [deprecated]\n"
|
||||
@" --json {path}: path to a JSON file containing a list of rules to add/remove\n"
|
||||
@"\n"
|
||||
@" Optionally:\n"
|
||||
@" --teamid: add or check a team ID rule instead of binary\n"
|
||||
@@ -81,7 +84,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 +120,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,6 +174,33 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
} else if ([arg caseInsensitiveCompare:@"--force"] == NSOrderedSame) {
|
||||
// Don't do anything special.
|
||||
#endif
|
||||
} else if ([arg caseInsensitiveCompare:@"--json"] == NSOrderedSame) {
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"--json requires an argument"];
|
||||
}
|
||||
jsonFilePath = arguments[i];
|
||||
} 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]];
|
||||
}
|
||||
@@ -189,6 +236,16 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
return [self printStateOfRule:newRule daemonConnection:self.daemonConn];
|
||||
}
|
||||
|
||||
// Note this block needs to come after the check block above.
|
||||
if (jsonFilePath.length > 0) {
|
||||
if (importRules) {
|
||||
[self importJSONFile:jsonFilePath];
|
||||
} else if (exportRules) {
|
||||
[self exportJSONFile:jsonFilePath];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (newRule.state == SNTRuleStateUnknown) {
|
||||
[self printErrorUsageAndExit:@"No state specified"];
|
||||
} else if (!newRule.identifier) {
|
||||
@@ -302,4 +359,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
|
||||
|
||||
@@ -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",
|
||||
@@ -470,6 +474,7 @@ objc_library(
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
"//Source/common:String",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -527,6 +532,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 +674,7 @@ objc_library(
|
||||
hdrs = ["Metrics.h"],
|
||||
deps = [
|
||||
":SNTApplicationCoreMetrics",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTXPCMetricServiceInterface",
|
||||
@@ -885,8 +893,10 @@ santa_unit_test(
|
||||
":Metrics",
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTDatabaseController",
|
||||
":SNTDecisionCache",
|
||||
":SNTEndpointSecurityAuthorizer",
|
||||
":SantadDeps",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:TestUtils",
|
||||
"@MOLCertificate",
|
||||
@@ -985,7 +995,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 +1246,7 @@ santa_unit_test(
|
||||
":WatchItems",
|
||||
"//Source/common:Platform",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:TestUtils",
|
||||
"@MOLCertificate",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -46,6 +46,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;
|
||||
@@ -509,6 +510,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);
|
||||
}
|
||||
|
||||
@@ -351,7 +351,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 +365,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 {
|
||||
|
||||
@@ -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,12 +35,13 @@
|
||||
#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;
|
||||
@@ -59,6 +59,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 +401,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());
|
||||
@@ -682,6 +683,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;
|
||||
@@ -157,7 +160,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 +239,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));
|
||||
@@ -767,7 +771,7 @@ void SerializeAndCheckNonESEvents(
|
||||
@"DADeviceVendor" : @"vendor",
|
||||
@"DADeviceModel" : @"model",
|
||||
@"DAAppearanceTime" : @(123456789),
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"path"],
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"/"],
|
||||
@"DAMediaBSDName" : @"bsd",
|
||||
@"DAVolumeKind" : @"apfs",
|
||||
@"DADeviceProtocol" : @"usb",
|
||||
@@ -792,6 +796,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.
|
||||
|
||||
@@ -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 = [[SNTMetricSet sharedInstance]
|
||||
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;
|
||||
|
||||
@@ -145,11 +145,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.
@@ -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
|
||||
|
||||
@@ -27,8 +27,6 @@
|
||||
<true/>
|
||||
<key>EnablePageZeroProtection</key>
|
||||
<false/>
|
||||
<key>EnableSystemExtension</key>
|
||||
<true/>
|
||||
<key>EventDetailText</key>
|
||||
<string>Open in moroz...</string>
|
||||
<key>EventDetailURL</key>
|
||||
|
||||
@@ -27,8 +27,6 @@
|
||||
<true/>
|
||||
<key>EnablePageZeroProtection</key>
|
||||
<false/>
|
||||
<key>EnableSystemExtension</key>
|
||||
<true/>
|
||||
<key>EventDetailText</key>
|
||||
<string>Open in moroz...</string>
|
||||
<key>EventDetailURL</key>
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
#!/bin/bash
|
||||
set -xo pipefail
|
||||
|
||||
function main() {
|
||||
GIT_ROOT=$(git rev-parse --show-toplevel)
|
||||
err=0
|
||||
GIT_ROOT=$(git rev-parse --show-toplevel)
|
||||
|
||||
find $GIT_ROOT \( -name "*.m" -o -name "*.h" -o -name "*.mm" -o -name "*.cc" \) -exec clang-format --Werror --dry-run {} \+
|
||||
err="$(( $err | $? ))"
|
||||
find $GIT_ROOT \( -name "*.m" -o -name "*.h" -o -name "*.mm" -o -name "*.cc" \) -exec clang-format --Werror --dry-run {} \+
|
||||
|
||||
! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'
|
||||
err="$(( $err | $? ))"
|
||||
! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'
|
||||
|
||||
go install github.com/bazelbuild/buildtools/buildifier@latest
|
||||
~/go/bin/buildifier --lint=warn -r $GIT_ROOT
|
||||
err="$(( $err | $? ))"
|
||||
go install github.com/bazelbuild/buildtools/buildifier@latest
|
||||
~/go/bin/buildifier --lint=warn -r $GIT_ROOT
|
||||
|
||||
python3 -m pip install -q pylint
|
||||
find $GIT_ROOT \( -name "*.py" \) -exec python3 -m pylint --score n --rcfile=$GIT_ROOT/.pylintrc {} \+
|
||||
err="$(( $err | $? ))"
|
||||
|
||||
return $err
|
||||
}
|
||||
|
||||
main $@
|
||||
exit $?
|
||||
python3 -m pip install -q pylint
|
||||
find $GIT_ROOT \( -name "*.py" \) -exec python3 -m pylint --score n --rcfile=$GIT_ROOT/.pylintrc {} \+
|
||||
|
||||
58
WORKSPACE
58
WORKSPACE
@@ -7,6 +7,22 @@ load(
|
||||
)
|
||||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
|
||||
# Abseil LTS branch, Aug 2023
|
||||
http_archive(
|
||||
name = "com_google_absl",
|
||||
strip_prefix = "abseil-cpp-20230802.0",
|
||||
urls = ["https://github.com/abseil/abseil-cpp/archive/refs/tags/20230802.0.tar.gz"],
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "com_google_protobuf",
|
||||
patch_args = ["-p1"],
|
||||
patches = ["//external_patches/com_google_protobuf:13636.patch"],
|
||||
sha256 = "07d69502e58248927b58c7d7e7424135272ba5b2852a753ab6b67e62d2d29355",
|
||||
strip_prefix = "protobuf-24.3",
|
||||
urls = ["https://github.com/protocolbuffers/protobuf/archive/v24.3.tar.gz"],
|
||||
)
|
||||
|
||||
# We don't directly use rules_python but several dependencies do and they disagree
|
||||
# about which version to use, so we force the latest.
|
||||
http_archive(
|
||||
@@ -18,28 +34,15 @@ http_archive(
|
||||
|
||||
http_archive(
|
||||
name = "build_bazel_rules_apple",
|
||||
sha256 = "f003875c248544009c8e8ae03906bbdacb970bc3e5931b40cd76cadeded99632", # 1.1.0
|
||||
urls = ["https://github.com/bazelbuild/rules_apple/releases/download/1.1.0/rules_apple.1.1.0.tar.gz"],
|
||||
sha256 = "8ac4c7997d863f3c4347ba996e831b5ec8f7af885ee8d4fe36f1c3c8f0092b2c",
|
||||
url = "https://github.com/bazelbuild/rules_apple/releases/download/2.5.0/rules_apple.2.5.0.tar.gz",
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_apple//apple:repositories.bzl", "apple_rules_dependencies")
|
||||
|
||||
apple_rules_dependencies()
|
||||
|
||||
load("@build_bazel_apple_support//lib:repositories.bzl", "apple_support_dependencies")
|
||||
|
||||
apple_support_dependencies()
|
||||
|
||||
http_archive(
|
||||
name = "build_bazel_rules_swift",
|
||||
sha256 = "84e2cc1c9e3593ae2c0aa4c773bceeb63c2d04c02a74a6e30c1961684d235593",
|
||||
url = "https://github.com/bazelbuild/rules_swift/releases/download/1.5.1/rules_swift.1.5.1.tar.gz",
|
||||
)
|
||||
|
||||
load(
|
||||
"@build_bazel_rules_swift//swift:repositories.bzl",
|
||||
"swift_rules_dependencies",
|
||||
)
|
||||
load("@build_bazel_rules_swift//swift:repositories.bzl", "swift_rules_dependencies")
|
||||
|
||||
swift_rules_dependencies()
|
||||
|
||||
@@ -50,6 +53,10 @@ load(
|
||||
|
||||
swift_rules_extra_dependencies()
|
||||
|
||||
load("@build_bazel_apple_support//lib:repositories.bzl", "apple_support_dependencies")
|
||||
|
||||
apple_support_dependencies()
|
||||
|
||||
# Hedron Bazel Compile Commands Extractor
|
||||
# Allows integrating with clangd
|
||||
# https://github.com/hedronvision/bazel-compile-commands-extractor
|
||||
@@ -72,23 +79,6 @@ http_archive(
|
||||
urls = ["https://github.com/google/googletest/archive/58d77fa8070e8cec2dc1ed015d66b454c8d78850.zip"],
|
||||
)
|
||||
|
||||
# Abseil - Abseil LTS branch, June 2022, Patch 1
|
||||
http_archive(
|
||||
name = "com_google_absl",
|
||||
sha256 = "b9f490fae1c0d89a19073a081c3c588452461e5586e4ae31bc50a8f36339135e",
|
||||
strip_prefix = "abseil-cpp-8c0b94e793a66495e0b1f34a5eb26bd7dc672db0",
|
||||
urls = ["https://github.com/abseil/abseil-cpp/archive/8c0b94e793a66495e0b1f34a5eb26bd7dc672db0.zip"],
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "com_google_protobuf",
|
||||
patch_args = ["-p1"],
|
||||
patches = ["//external_patches/com_google_protobuf:10120.patch"],
|
||||
sha256 = "73c95c7b0c13f597a6a1fec7121b07e90fd12b4ed7ff5a781253b3afe07fc077",
|
||||
strip_prefix = "protobuf-3.21.6",
|
||||
urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.21.6.tar.gz"],
|
||||
)
|
||||
|
||||
# Note: Protobuf deps must be loaded after defining the ABSL archive since
|
||||
# protobuf repo would pull an in earlier version of ABSL.
|
||||
load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
|
||||
@@ -195,8 +185,8 @@ git_repository(
|
||||
shallow_since = "1594986926 -0400",
|
||||
)
|
||||
|
||||
load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
|
||||
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
|
||||
load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
|
||||
load("//external_patches/moroz:deps.bzl", "moroz_dependencies")
|
||||
|
||||
# gazelle:repository_macro external_patches/moroz/deps.bzl%moroz_dependencies
|
||||
|
||||
@@ -7,8 +7,7 @@ parent: Concepts
|
||||
## Rule Types
|
||||
|
||||
Rules provide the primary evaluation mechanism for allowing and blocking
|
||||
binaries with Santa on macOS. There are four types of rules: binary, signing ID,
|
||||
certificate, and Team ID.
|
||||
binaries with Santa on macOS.
|
||||
|
||||
### Binary Rules
|
||||
|
||||
@@ -86,6 +85,13 @@ as a single developer account can and frequently will request/rotate between
|
||||
multiple different signing certificates and entitlements. This is an even more
|
||||
powerful rule with broader reach than individual certificate rules.
|
||||
|
||||
### Compiler/Transitive Rules
|
||||
|
||||
The transitive allowlist capability of Santa can automatically allowlist any files that are created by a set of specified binaries. A typical use-case is allowing any binaries compiled with XCode on developer machines to execute, as it would be slow and impractical to use other rule types to permit these.
|
||||
|
||||
To begin using transitive allowlisting, `EnableTransitiveRules` should be set to true and Compiler rules (rules with the policy `ALLOWLIST_COMPILER`) should be added to indicate the binaries which will be writing the new files to be allowlisted. Only rules of type `BINARY` and `SIGNINGID` are allowed for compiler rules. Santa will create and manage Transitive rules in its database automatically, they cannot be created directly.
|
||||
|
||||
|
||||
## Rule Evaluation
|
||||
|
||||
When a process is trying to execute, `santad` retrieves information on the
|
||||
|
||||
@@ -71,9 +71,11 @@ also known as mobileconfig files, which are in an Apple-specific XML format.
|
||||
| DisableUnknownEventUpload | Bool | If YES, the client will *not* upload events for executions of unknown binaries allowed in monitor mode |
|
||||
| BlockUSBMount | Bool | If set to 'True' blocking USB Mass storage feature is enabled. Defaults to `False`. |
|
||||
| RemountUSBMode | Array | Array of strings for arguments to pass to mount -o (any of "rdonly", "noexec", "nosuid", "nobrowse", "noowners", "nodev", "async", "-j"). when forcibly remounting devices. No default. |
|
||||
| FileAccessPolicyPlist | String | (BETA) Path to a file access configuration plist. |
|
||||
| FileAccessPolicyUpdateIntervalSec | Integer | (BETA) Number of seconds between re-reading the file access policy config and policies/monitored paths updated. |
|
||||
| SyncClientContentEncoding | String | Sets the Content-Encoding header for requests sent to the sync service. Acceptable values are "deflate", "gzip", "none" (Defaults to deflate.) |
|
||||
| FileAccessPolicyPlist | String | Path to a file access configuration plist. This is ignored if `FileAccessPolicy` is also set. |
|
||||
| FileAccessPolicy | Dictionary | A complete file access configuration policy embedded in the main Santa config. If set, `FileAccessPolicyPlist` will be ignored. |
|
||||
| FileAccessPolicyUpdateIntervalSec | Integer | Number of seconds between re-reading the file access policy config and policies/monitored paths updated. |
|
||||
| SyncClientContentEncoding | String | Sets the Content-Encoding header for requests sent to the sync service. Acceptable values are "deflate", "gzip", "none" (Defaults to deflate.) |
|
||||
| SyncExtraHeaders | Dictionary | Dictionary of additional headers to include in all requests made to the sync server. System managed headers such as Content-Length, Host, WWW-Authenticate etc will be ignored. |
|
||||
|
||||
|
||||
\*overridable by the sync server: run `santactl status` to check the current
|
||||
@@ -159,6 +161,11 @@ A few key points to when creating your configuration profile:
|
||||
<string>https://sync-server-hostname/api/santa/</string>
|
||||
<key>UnknownBlockMessage</key>
|
||||
<string>This application has been blocked from executing.</string>
|
||||
<key>SyncExtraHeaders</key>
|
||||
<dict>
|
||||
<key>X-API-Key</key>
|
||||
<string>da823De3Dsadq3234dqwe32kv</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
@@ -4,9 +4,7 @@ parent: Deployment
|
||||
nav_order: 4
|
||||
---
|
||||
|
||||
# (BETA) File Access Authorization
|
||||
|
||||
> **IMPORTANT:** This feature is in beta. Configuration and log formats are subject to change.
|
||||
# File Access Authorization
|
||||
|
||||
> **IMPORTANT:** This feature is only supported on macOS 13 and above.
|
||||
|
||||
@@ -21,6 +19,8 @@ To enable this feature, the `FileAccessPolicyPlist` key in the main [Santa confi
|
||||
| Key | Parent | Type | Required | Santa Version | Description |
|
||||
| :------------------------ | :----------- | :--------- | :------- | :------------ | :---------- |
|
||||
| `Version` | `<Root>` | String | Yes | v2023.1+ | Version of the configuration. Will be reported in events. |
|
||||
| `EventDetailURL` | `<Root>` | String | No | v2023.8+ | When the user gets a block notification, a button can be displayed which will take them to a web page with more information about that event. This URL will be used for all rules unless overridden by a rule-specific option. See the [EventDetailURL](#eventdetailurl) section below. |
|
||||
| `EventDetailText` | `<Root>` | String | No | v2023.8+ | Related to `EventDetailURL`, controls the button text that will be displayed. (max length = 48 chars) |
|
||||
| `WatchItems` | `<Root>` | Dictionary | No | v2023.1+ | The set of configuration items that will be monitored by Santa. |
|
||||
| `<Name>` | `WatchItems` | Dictionary | No | v2023.1+ | A unique name that identifies a single watch item rule. This value will be reported in events. The name must be a legal C identifier (i.e., must conform to the regex `[A-Za-z_][A-Za-z0-9_]*`). |
|
||||
| `Paths` | `<Name>` | Array | Yes | v2023.1+ | A list of either String or Dictionary types that contain path globs to monitor. String type entires will have default values applied for the attributes that can be manually set with the Dictionary type. |
|
||||
@@ -30,6 +30,8 @@ To enable this feature, the `FileAccessPolicyPlist` key in the main [Santa confi
|
||||
| `AllowReadAccess` | `Options` | Boolean | No | v2023.1+ | If true, indicates the rule will **not** be applied to actions that are read-only access (e.g., opening a watched path for reading, or cloning a watched path). If false, the rule will apply both to read-only access and access that could modify the watched path. (Default = `false`) |
|
||||
| `AuditOnly` | `Options` | Boolean | No | v2023.1+ | If true, operations violating the rule will only be logged. If false, operations violating the rule will be denied and logged. (Default = `true`) |
|
||||
| `InvertProcessExceptions` | `Options` | Boolean | No | v2023.5+ | If true, logic is inverted for the list of processes defined by the `Processes` key such that the list becomes the set of processes that will be denied or allowed but audited. (Default = `false`) |
|
||||
| `EventDetailURL` | `Options` | String | No | v2023.8+ | Rule-specific URL that overrides the top-level `EventDetailURL`. |
|
||||
| `EventDetailText` | `Options` | String | No | v2023.8+ | Rule-specific button text that overrides the top-level `EventDetailText`. |
|
||||
| `Processes` | `<Name>` | Array | No | v2023.1+ | A list of dictionaries defining processes that are allowed to access paths matching the globs defined with the `Paths` key. For a process performing the operation to be considered a match, it must match all defined attributes of at least one entry in the list. |
|
||||
| `BinaryPath` | `Processes` | String | No | v2023.1+ | A path literal that an instigating process must be executed from. |
|
||||
| `TeamID` | `Processes` | String | No | v2023.1+ | Team ID of the instigating process. |
|
||||
@@ -38,6 +40,25 @@ To enable this feature, the `FileAccessPolicyPlist` key in the main [Santa confi
|
||||
| `SigningID` | `Processes` | String | No | v2023.1+ | Signing ID of the instigating process. |
|
||||
| `PlatformBinary` | `Processes` | Boolean | No | v2023.2+ | Whether or not the instigating process is a platform binary. |
|
||||
|
||||
### EventDetailURL
|
||||
When the user gets a file access block notification, a button can be displayed
|
||||
which will take them to a web page with more information about that event.
|
||||
|
||||
This property contains a kind of format string to be turned into the URL to send
|
||||
them to. The following sequences will be replaced in the final URL:
|
||||
|
||||
| Key | Description |
|
||||
| :------------------ | :---------- |
|
||||
| `%rule_version%` | Version of the rule that was violated |
|
||||
| `%rule_name%` | Name of the rule that was violated |
|
||||
| `%file_identifier%` | SHA-256 of the binary being executed |
|
||||
| `%accessed_path%` | The path accessed by the binary |
|
||||
| `%username%` | The executing user |
|
||||
| `%machine_id%` | ID of the machine |
|
||||
| `%serial%` | System's serial number |
|
||||
| `%uuid%` | System's UUID |
|
||||
| `%hostname%` | System's full hostname |
|
||||
|
||||
### Example Configuration
|
||||
|
||||
This is an example configuration conforming to the specification outlined above:
|
||||
|
||||
@@ -44,7 +44,7 @@ Santa as well as its details and live connection state
|
||||
|
||||
Looking into [logs](../concepts/logs.md) would be instructive for the majority
|
||||
of how Santa is operating, and the pages on [scopes](../concepts/scopes.md) and [rules](../concepts/rules.md) would assist in
|
||||
determining precendence and why decisions are made. Most helpful is the output of
|
||||
determining precedence and why decisions are made. Most helpful is the output of
|
||||
`/usr/local/bin/santactl`'s `fileinfo` verb when called with the path/binary in
|
||||
question as described on the [santactl](../binaries/santactl.md) page.
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ sequenceDiagram
|
||||
client ->> server: POST /ruledownload/<machine_id>
|
||||
server --> client: ruledownload response (200)
|
||||
end
|
||||
client ->> server: POST /postflight/<machine_id> request
|
||||
client ->> server: POST /postflight/<machine_id>
|
||||
server -->> client: postflight response (200)
|
||||
```
|
||||
|
||||
@@ -92,13 +92,13 @@ The request consists of the following JSON keys:
|
||||
| model_identifier | NO | string | The model of the macOS system | |
|
||||
| santa_version | YES | string | 2022.3 |
|
||||
| primary_user | YES | string | The username | markowsky |
|
||||
| binary_rule_count | NO | int | Number of binary allow / deny rules the client has at time of sync| 1000 |
|
||||
| binary_rule_count | NO | int | Number of binary allow / deny rules the client has at time of sync | 1000 |
|
||||
| certificate_rule_count | NO | int | Number of certificate allow / deny rules the client has at time of sync | 3400 |
|
||||
| compiler_rule_count | NO | int | Number of compiler rules the client has time of sync |
|
||||
| transitive_rule_count | NO | int | Number of transitive rules the client has at the time of sync |
|
||||
| teamid_rule_count | NO | int | Number of TeamID rules the client has at the time of sync | 24 |
|
||||
| client_mode | YES | string | the mode the client is operating in, either "LOCKDOWN" or "MONITOR" | LOCKDOWN |
|
||||
| request_clean_sync | NO | bool | the client has requested a clean sync of its rules from the server. | true |
|
||||
| client_mode | YES | string | The mode the client is operating in, either "LOCKDOWN" or "MONITOR" | LOCKDOWN |
|
||||
| request_clean_sync | NO | bool | The client has requested a clean sync of its rules from the server | true |
|
||||
|
||||
|
||||
### Example preflight request JSON Payload:
|
||||
@@ -118,7 +118,7 @@ The request consists of the following JSON keys:
|
||||
"transitive_rule_count" : 0,
|
||||
"os_version" : "12.4",
|
||||
"model_identifier" : "MacBookPro15,1",
|
||||
"request_clean_sync": true,
|
||||
"request_clean_sync": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -130,16 +130,17 @@ The JSON object has the following keys:
|
||||
|
||||
| Key | Required | Type | Meaning | Example Value |
|
||||
|---|---|---|---|---|
|
||||
| enable_bundles | NO | boolean | Enabled bundle scanning | true |
|
||||
| enable_bundles | NO | boolean | Enable bundle scanning | true |
|
||||
| enable_transitive_rules | NO | boolean | Whether or not to enable transitive allowlisting | true |
|
||||
| batch_size | YES | integer | Number of events to upload at a time | 128 |
|
||||
| full_sync_interval | YES | integer | Number of seconds between full syncs | 600 |
|
||||
| client_mode | YES | string | Operating mode to set for the client | either "MONITOR" or "LOCKDOWN" |
|
||||
| allowed_path_regex | NO | string | Regular expression to allow a binary to execute from a path | "/Users/markowsk/foo/.*" |
|
||||
| allowed_path_regex | NO | string | Regular expression to allow a binary to execute from a path | "/Users/markowsk/foo/.\*" |
|
||||
| blocked_path_regex | NO | string | Regular expression to block a binary from executing by path | "/tmp/" |
|
||||
| block_usb_mount | NO | boolean | Block USB mass storage devices | true |
|
||||
| remount_usb_mode | NO | string | Force USB mass storage devices to be remounted with the following permissions (see [configuration](../deployment/configuration.md)) | |
|
||||
| clean_sync | YES | boolean | Whether or not the rules should be dropped and synced entirely from the server | true |
|
||||
| override_file_access_action | NO | string | Override file access config policy action. Must be:<br />1.) "Disable" to not log or block any rule violations.<br />2.) "AuditOnly" to only log violations, not block anything.<br />3.) "" (empty string) or "None" to not override the config | "Disable", or "AuditOnly", or "" (empty string) |
|
||||
|
||||
#### Example Preflight Response Payload
|
||||
|
||||
@@ -182,36 +183,39 @@ sequenceDiagram
|
||||
|
||||
| Key | Required | Type | Meaning | Example Value |
|
||||
|---|---|---|---|---|
|
||||
| file_sha256 | YES | string | sha256 of the executable that was executed | "fc6679da622c3ff38933220b8e73c7322ecdc94b4570c50ecab0da311b292682" |
|
||||
| file_sha256 | YES | string | SHA256 hash of the executable that was executed | "fc6679da622c3ff38933220b8e73c7322ecdc94b4570c50ecab0da311b292682" |
|
||||
| file_path | YES | string | Absolute file path to the executable that was blocked | "/tmp/foo" |
|
||||
| file_name | YES | string | Command portion of the path of the blocked executable | "foo" |
|
||||
| executing_user | YES | string | Username that executed the binary | "markowsky" |
|
||||
| execution_time | YES | float64 | Unix timestamp of when the execution occured | 23344234232 |
|
||||
| loggedin_users | NO | List of strings | list of usernames logged in according to utmp | ["markowsky"] |
|
||||
| current_sessions | YES | List of strings | list of user sessions | ["markowsky@console", "markowsky@ttys000"] |
|
||||
| decision | YES | string | The decision Santa made for this binary, BUNDLE_BINARY is used to preemptively report binaries in a bundle. **Must be one of the examples**.| "ALLOW_BINARY", "ALLOW_CERTIFICATE", "ALLOW_SCOPE", "ALLOW_TEAMID", "ALLOW_UNKNOWN", "BLOCK_BINARY", "BLOCK_CERTIFICATE", "BLOCK_SCOPE", "BLOCK_TEAMID", "BLOCK_UNKNOWN", "BUNDLE_BINARY" |
|
||||
| executing_user | NO | string | Username that executed the binary | "markowsky" |
|
||||
| execution_time | NO | float64 | Unix timestamp of when the execution occured | 23344234232 |
|
||||
| loggedin_users | NO | list of strings | List of usernames logged in according to utmp | ["markowsky"] |
|
||||
| current_sessions | NO | list of strings | List of user sessions | ["markowsky@console", "markowsky@ttys000"] |
|
||||
| decision | YES | string | The decision Santa made for this binary, BUNDLE_BINARY is used to preemptively report binaries in a bundle. **Must be one of the examples** | "ALLOW_BINARY", "ALLOW_CERTIFICATE", "ALLOW_SCOPE", "ALLOW_TEAMID", "ALLOW_UNKNOWN", "BLOCK_BINARY", "BLOCK_CERTIFICATE", "BLOCK_SCOPE", "BLOCK_TEAMID", "BLOCK_UNKNOWN", "BUNDLE_BINARY" |
|
||||
| file_bundle_id | NO | string | The executable's containing bundle's identifier as specified in the Info.plist | "com.apple.safari" |
|
||||
| file_bundle_path | NO | string | The path that the bundle resids in | /Applications/Santa.app |
|
||||
| file_bundle_executable_rel_path | NO | string | The relative path of the binary within the Bundle | "Contents/MacOS/AppName" |
|
||||
| file_bundle_name | NO | string | The bundle's display name. | "Google Chrome" |
|
||||
| file_bundle_name | NO | string | The bundle's display name | "Google Chrome" |
|
||||
| file_bundle_version | NO | string | The bundle version string | "9999.1.1" |
|
||||
| file_bundle_version_string | NO | string | Bundle short version string | "2.3.4" |
|
||||
| file_bundle_hash | NO | string | SHA256 hash of all executables in the bundle | "7466e3687f540bcb7792c6d14d5a186667dbe18a85021857b42effe9f0370805" |
|
||||
| file_bundle_hash_millis | NO | float64 | The time in milliseconds it took to find all of the binaries, hash and produce the bundle_hash | 1234775 |
|
||||
| pid | YES | int | Process id of the executable that was blocked | 1234 |
|
||||
| ppid | YES | int | Parent process id of the executable that was blocked | 456 |
|
||||
| parent_name | YES | Parent process short command name of the executable that was blocked | "bar" |
|
||||
| file_bundle_binary_count | NO | integer | The number of binaries in a bundle | 13 |
|
||||
| pid | NO | int | Process id of the executable that was blocked | 1234 |
|
||||
| ppid | NO | int | Parent process id of the executable that was blocked | 456 |
|
||||
| parent_name | NO | Parent process short command name of the executable that was blocked | "bar" |
|
||||
| quarantine_data_url | NO | string | The actual URL of the quarantined item from the quarantine database that this binary was downloaded from | https://dl.google.com/chrome/mac/stable/GGRO/googlechrome.dmg |
|
||||
| quarantine_referer_url | NO | string | Referring URL that lead to the binary being downloaded if known. | https://www.google.com/chrome/downloads/ |
|
||||
| quarantine_referer_url | NO | string | Referring URL that lead to the binary being downloaded if known | https://www.google.com/chrome/downloads/ |
|
||||
| quarantine_timestamp | NO | float64 | Unix Timestamp of when the binary was downloaded or 0 if not quarantined | 0 |
|
||||
| quarantine_agent_bundle_id | NO | string | The bundle ID of the software that quarantined the binary | "com.apple.Safari" |
|
||||
| signing_chain | NO | list of signing chain objects | Certs used to code sign the executable | See next section |
|
||||
| signing_id | NO | string | Signing ID of the binary that was executed | "EQHXZ8M8AV:com.google.Chrome" |
|
||||
| team_id | NO | string | Team ID of the binary that was executed | "EQHXZ8M8AV" |
|
||||
|
||||
##### Signing Chain Objects
|
||||
|
||||
| Key | Required | Type | Meaning | Example Value |
|
||||
|---|---|---|---|---|
|
||||
| sha256 | YES | string | sha256 of the certificate used to sign | "7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258" |
|
||||
| sha256 | YES | string | SHA256 thumbprint of the certificate used to sign | "7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258" |
|
||||
| cn | YES | string | Common Name field of the certificate used to sign | "Apple Worldwide Developer Relations Certification Authority" |
|
||||
| org | YES | string | Org field of the certificate used to sign | "Google LLC" |
|
||||
| ou | YES | string | OU field of the certificate used to sign | "G3" |
|
||||
@@ -232,11 +236,11 @@ sequenceDiagram
|
||||
],
|
||||
"quarantine_timestamp": 0,
|
||||
"signing_chain": [{
|
||||
"cn": "Apple Development: Google Development (XXXXXXXXXX)",
|
||||
"cn": "Apple Development: Google Development (EQHXZ8M8AV)",
|
||||
"valid_until": 1678983513,
|
||||
"org": "Google LLC",
|
||||
"valid_from": 1647447514,
|
||||
"ou": "XXXXXXXXXX",
|
||||
"ou": "EQHXZ8M8AV",
|
||||
"sha256": "7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
},
|
||||
{
|
||||
@@ -273,7 +277,8 @@ sequenceDiagram
|
||||
"markowsky@ttys001",
|
||||
"markowsky@ttys003"
|
||||
],
|
||||
"team_id": "XXXXXXXXXX"
|
||||
"team_id": "EQHXZ8M8AV",
|
||||
"signing_id": "EQHXZ8M8AV:com.google.santa"
|
||||
}]
|
||||
}
|
||||
```
|
||||
@@ -380,18 +385,14 @@ downloading if the rules need to be downloaded in multiple batches.
|
||||
"rule_type": "BINARY",
|
||||
"policy": "ALLOWLIST",
|
||||
"custom_msg": "",
|
||||
"creation_time": 1573572118.380034,
|
||||
"file_bundle_binary_count": 13,
|
||||
"file_bundle_hash": "7466e3687f540bcb7792c6d14d5a186667dbe18a85021857b42effe9f0370805"
|
||||
"creation_time": 1573572118.380034
|
||||
},
|
||||
{
|
||||
"identifier": "EQHXZ8M8AV",
|
||||
"rule_type": "TEAMID",
|
||||
"policy": "ALLOWLIST",
|
||||
"custom_msg": "Allow Software Google's Team ID",
|
||||
"creation_time": 1576623399.151607,
|
||||
"file_bundle_binary_count": 7,
|
||||
"file_bundle_hash": "e4736dd3a731f5f71850984175c0ec54dcde06021af18f476eb480c707fbecda"
|
||||
"creation_time": 1576623399.151607
|
||||
}],
|
||||
"cursor": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXzfmdvb2dsZS5jb206YXBwbm90aHJyYAsSCUJsb2NrYWJsZSJANGYyYTA2MjY1ZjRiODQ2M2Y2YjI0MmNiZTMwMTNkMGZhNjlkNDUxNmI4OTU3Y2I3ZDAxZDcyMTJkM2NhZmZiNAwLEgRSdWxlGICA8Kehk9MKDBgAIAA="
|
||||
}
|
||||
@@ -411,7 +412,22 @@ sequenceDiagram
|
||||
|
||||
#### `postflight` Request
|
||||
|
||||
The request is empty and should not be parsed by the sync server.
|
||||
The request consists of the following JSON keys:
|
||||
|
||||
| Key | Required | Type | Meaning | Example Value |
|
||||
|---|---|---|---|---|
|
||||
| rules_received | YES | int | The number of rules the client received from all ruledownlaod requests. | 211 |
|
||||
| rules_processed | YES | int | The number of rules that were processed from all ruledownload requests. | 212 |
|
||||
|
||||
### Example postflight request JSON Payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"rules_received" : 211,
|
||||
"rules_processed" : 212
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### `postflight` Response
|
||||
|
||||
|
||||
BIN
docs/images/santa-block.gif
Normal file
BIN
docs/images/santa-block.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 613 KiB |
@@ -34,7 +34,7 @@ The following pages give an overview of how Santa accomplishes authorization at
|
||||
|
||||
* [Getting Started](deployment/getting-started.md): A quick guide to setting up your deployment.
|
||||
* [Configuration](deployment/configuration.md): The local and sync server configuration options, along with example needed mobileconfig files.
|
||||
* (BETA) [File Access Authorization](deployment/file-access-auth.md): Guide to enabling the feature and details about its configuration and operation.
|
||||
* [File Access Authorization](deployment/file-access-auth.md): Guide to enabling the feature and details about its configuration and operation.
|
||||
* [Sync Servers](deployment/sync-servers.md): A list of open-source sync servers.
|
||||
* [Troubleshooting](deployment/troubleshooting.md): How to troubleshoot issues with your Santa deployment.
|
||||
|
||||
|
||||
@@ -36,12 +36,10 @@ syncs, listen for push notifications and upload events.
|
||||
sending rules from the sync server to process.
|
||||
3. The full sync starts. There are a number of stages to a full sync:
|
||||
1. preflight: The sync server can set various settings for Santa.
|
||||
2. logupload (optional): The sync server can request that the Santa logs be
|
||||
uploaded to an endpoint.
|
||||
3. eventupload (optional): If Santa has generated events, it will upload
|
||||
2. eventupload (optional): If Santa has generated events, it will upload
|
||||
them to the sync-server.
|
||||
4. ruledownload: Download rules from the sync server.
|
||||
5. postflight: Updates timestamps for successful syncs.
|
||||
3. ruledownload: Download rules from the sync server.
|
||||
4. postflight: Updates timestamps for successful syncs.
|
||||
4. After the full sync completes a new full sync will be scheduled, by default
|
||||
this will be 10min. However there are a few ways to manipulate this:
|
||||
1. The sync server can send down a configuration in the preflight to
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
From b635b9632885bfc16768fc8bbd33e15ff6a0da77 Mon Sep 17 00:00:00 2001
|
||||
From: Marc Plano-Lesay <mplano@google.com>
|
||||
Date: Thu, 9 Jun 2022 08:59:00 +1000
|
||||
Subject: [PATCH] Remove unused Offset function
|
||||
|
||||
This fails to build with -Werror.
|
||||
---
|
||||
src/google/protobuf/generated_message_tctable_lite.cc | 5 -----
|
||||
1 file changed, 5 deletions(-)
|
||||
|
||||
diff --git a/src/google/protobuf/generated_message_tctable_lite.cc b/src/google/protobuf/generated_message_tctable_lite.cc
|
||||
index 519b10776c..b98b00c411 100644
|
||||
--- a/src/google/protobuf/generated_message_tctable_lite.cc
|
||||
+++ b/src/google/protobuf/generated_message_tctable_lite.cc
|
||||
@@ -340,11 +340,6 @@ const char* TcParser::MiniParse(PROTOBUF_TC_PARAM_DECL) {
|
||||
|
||||
namespace {
|
||||
|
||||
-// Offset returns the address `offset` bytes after `base`.
|
||||
-inline void* Offset(void* base, uint32_t offset) {
|
||||
- return static_cast<uint8_t*>(base) + offset;
|
||||
-}
|
||||
-
|
||||
// InvertPacked changes tag bits from the given wire type to length
|
||||
// delimited. This is the difference expected between packed and non-packed
|
||||
// repeated fields.
|
||||
15
external_patches/com_google_protobuf/13636.patch
Normal file
15
external_patches/com_google_protobuf/13636.patch
Normal file
@@ -0,0 +1,15 @@
|
||||
diff --git a/protobuf_deps.bzl b/protobuf_deps.bzl
|
||||
index ee552c13e..f0bba0385 100644
|
||||
--- a/protobuf_deps.bzl
|
||||
+++ b/protobuf_deps.bzl
|
||||
@@ -70,8 +70,8 @@ def protobuf_deps():
|
||||
_github_archive(
|
||||
name = "utf8_range",
|
||||
repo = "https://github.com/protocolbuffers/utf8_range",
|
||||
- commit = "de0b4a8ff9b5d4c98108bdfe723291a33c52c54f",
|
||||
- sha256 = "5da960e5e5d92394c809629a03af3c7709d2d3d0ca731dacb3a9fb4bf28f7702",
|
||||
+ commit = "d863bc33e15cba6d873c878dcca9e6fe52b2f8cb",
|
||||
+ sha256 = "568988b5f7261ca181468dba38849fabf59dd9200fb2ed4b2823da187ef84d8c",
|
||||
)
|
||||
|
||||
if not native.existing_rule("rules_cc"):
|
||||
Reference in New Issue
Block a user