Compare commits

...

14 Commits

Author SHA1 Message Date
Russell Hancox
d82e64aa5f Project: Split integration VM license into its own LICENSE file (#1147) 2023-08-08 13:29:06 -04:00
Ivan Tadeu Ferreira Antunes Filho
a9c1c730be Project: Cast some enums to int (#1146)
Allows the string displaying the enum to format it using %d.

Fixes the error: `error: format specifies type 'int' but the argument has type 'T' [-Werror,-Wformat]`
2023-08-08 13:08:11 -04:00
Matt W
6c4362d8bb Add hot cache for targets of read only policies (#1145)
* Add hot cache for file reads

* Clear cache on policy change

* Prevent unbounded cache growth

* Move cache impl to its own class

* Add some additional tests

* Cleanup

* Comment cleanup

* Switch to absl containers

* Use default absl::Hash instead of custom hasher

* Removing another reference to PairHash

* Remove unused imports
2023-08-08 12:38:33 -04:00
Russell Hancox
c1189493e8 sync/UI: Add ability to send custom URLs for blocking rules. (#1140)
This allows a sync server to send a `custom_url` field along with a rule blocking execution and this will be used as the URL for the "open" button in place of the normally generated URL.
2023-08-04 15:01:45 -04:00
Russell Hancox
aaa0d40841 sync: Add SyncExtraHeaders config option. (#1144)
* sync: Add SyncExtraHeaders config option.

Fixes #1143
2023-08-03 23:16:59 -04:00
Matt W
a424c4afca Only update daemon settings when sync settings explicitly set (#1142) 2023-08-03 16:18:40 -04:00
Matt W
2847397b66 Have distributed notifications delivered immediately (#1141) 2023-08-03 15:49:10 -04:00
Matt W
ad8b4b6646 Check if spool dir has changed before estimating size (#1138) 2023-08-03 14:54:14 -04:00
alexgraehl
39ee9e7d48 sync: Change backoff in SNTSyncStage.m to be exponential (#1135)
* Change backoff in SNTSyncStage.m to be exponential instead of linear
* Improves the log message to indicate that the job will ALWAYS abort after N retries. Previously, it was not clear why it would try 5 times and then give up.
2023-07-26 15:55:37 -04:00
Matt W
3cccacc3fb Add additional dep to satisfy import issue (#1134) 2023-07-26 12:50:29 -04:00
Matt W
6ed5bcd808 Enforce expected case for various rule type identifiers (#1132)
* Bump DB version. Ensure proper casing for rule identifiers on insert.

* Minor comment fixes, more test cases

* Handle SigningIDs using the delimiter character

* lint

* PR feedback
2023-07-26 12:31:28 -04:00
Matt W
bcac65a23e Wire up TTYWriter instance to the file access client (#1129) 2023-07-26 00:11:36 -04:00
Matt W
03fcd0c906 Add more file access config options (#1128)
* New file access config options supporting silencing and custom messages

* Rename custom message key
2023-07-26 00:01:04 -04:00
p-harrison
d3b71a3ba8 Update sync-protocol.md to include SIGNINGID rule type (#1130)
* Update sync-protocol.md

Couple of formatting changes, added SIGNINGID as a rule type

* Update docs/development/sync-protocol.md

Co-authored-by: Matt W <436037+mlw@users.noreply.github.com>

---------

Co-authored-by: Matt W <436037+mlw@users.noreply.github.com>
2023-07-25 14:31:13 -04:00
53 changed files with 983 additions and 252 deletions

View File

@@ -201,12 +201,3 @@
See the License for the specific language governing permissions and
limitations under the License.
------------------
Files: Testing/integration/VM/*
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -238,7 +238,10 @@ objc_library(
santa_unit_test(
name = "SNTRuleTest",
srcs = ["SNTRuleTest.m"],
deps = [":SNTRule"],
deps = [
":SNTCommonEnums",
":SNTRule",
],
)
objc_library(

View File

@@ -42,7 +42,7 @@
/// Return a URL generated from the EventDetailURL configuration key
/// after replacing templates in the URL with values from the event.
///
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event;
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event customURL:(NSString *)url;
///
/// Strip HTML from a string, replacing <br /> with newline.

View File

@@ -109,14 +109,31 @@
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event {
// Returns either the generated URL for the passed in event, or an NSURL from the passed in custom
// URL string. If the custom URL string is the string "null", nil will be returned. If no custom
// URL is passed and there is no configured EventDetailURL template, nil will be returned.
// The following "format strings" will be replaced in the URL, if they are present:
//
// %file_identifier% - The SHA-256 of the binary being executed.
// %bundle_or_file_identifier% - The hash of the bundle containing this file or the file itself,
// if no bundle hash is present.
// %username% - The executing user's name.
// %machine_id% - The configured machine ID for this host.
// %hostname% - The machine's FQDN.
// %uuid% - The machine's UUID.
// %serial% - The machine's serial number.
//
+ (NSURL *)eventDetailURLForEvent:(SNTStoredEvent *)event customURL:(NSString *)url {
SNTConfigurator *config = [SNTConfigurator configurator];
NSString *hostname = [SNTSystemInfo longHostname];
NSString *uuid = [SNTSystemInfo hardwareUUID];
NSString *serial = [SNTSystemInfo serialNumber];
NSString *formatStr = config.eventDetailURL;
NSString *formatStr = url;
if (!url.length) formatStr = config.eventDetailURL;
if (!formatStr.length) return nil;
if ([formatStr isEqualToString:@"null"]) return nil;
if (event.fileSHA256) {
// This key is deprecated, use %file_identifier% or %bundle_or_file_identifier%
@@ -148,7 +165,9 @@
formatStr = [formatStr stringByReplacingOccurrencesOfString:@"%serial%" withString:serial];
}
return [NSURL URLWithString:formatStr];
NSURL *u = [NSURL URLWithString:formatStr];
if (!u) LOGW(@"Unable to generate event detail URL for string '%@'", formatStr);
return u;
}
@end

View File

@@ -42,6 +42,7 @@
@property NSString *quarantineURL;
@property NSString *customMsg;
@property NSString *customURL;
@property BOOL silentBlock;
@end

View File

@@ -390,6 +390,27 @@
///
@property(readonly, nonatomic) NSDictionary *syncProxyConfig;
///
/// Extra headers to include in all requests made during syncing.
/// Keys and values must all be strings, any other type will be silently ignored.
/// Some headers cannot be set through this key, including:
///
/// * Content-Encoding
/// * Content-Length
/// * Content-Type
/// * Connection
/// * Host
/// * Proxy-Authenticate
/// * Proxy-Authorization
/// * WWW-Authenticate
///
/// The header "Authorization" is also documented by Apple to be one that will
/// be ignored but this is not really the case, at least at present. If you
/// are able to use a different header for this that would be safest but if not
/// using Authorization /should/ be fine.
///
@property(readonly, nonatomic) NSDictionary *syncExtraHeaders;
///
/// The machine owner.
///

View File

@@ -57,6 +57,7 @@ static NSString *const kMobileConfigDomain = @"com.google.santa";
static NSString *const kStaticRules = @"StaticRules";
static NSString *const kSyncBaseURLKey = @"SyncBaseURL";
static NSString *const kSyncProxyConfigKey = @"SyncProxyConfiguration";
static NSString *const kSyncExtraHeadersKey = @"SyncExtraHeaders";
static NSString *const kSyncEnableCleanSyncEventUpload = @"SyncEnableCleanSyncEventUpload";
static NSString *const kClientAuthCertificateFileKey = @"ClientAuthCertificateFile";
static NSString *const kClientAuthCertificatePasswordKey = @"ClientAuthCertificatePassword";
@@ -196,6 +197,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kSyncBaseURLKey : string,
kSyncEnableCleanSyncEventUpload : number,
kSyncProxyConfigKey : dictionary,
kSyncExtraHeadersKey : dictionary,
kClientAuthCertificateFileKey : string,
kClientAuthCertificatePasswordKey : string,
kClientAuthCertificateCNKey : string,
@@ -315,6 +317,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingSyncExtraHeaders {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingEnableCleanSyncEventUpload {
return [self configStateSet];
}
@@ -632,6 +638,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return self.configState[kSyncProxyConfigKey];
}
- (NSDictionary *)syncExtraHeaders {
return self.configState[kSyncExtraHeadersKey];
}
- (BOOL)enablePageZeroProtection {
NSNumber *number = self.configState[kEnablePageZeroProtectionKey];
return number ? [number boolValue] : YES;

View File

@@ -41,6 +41,11 @@
///
@property(copy) NSString *customMsg;
///
/// A custom URL to take the user to when this binary is blocked from executing.
///
@property(copy) NSString *customURL;
///
/// The time when this rule was last retrieved from the rules database, if rule is transitive.
/// Stored as number of seconds since 00:00:00 UTC on 1 January 2001.

View File

@@ -13,8 +13,15 @@
/// limitations under the License.
#import "Source/common/SNTRule.h"
#include <CommonCrypto/CommonCrypto.h>
#include <os/base.h>
#import "Source/common/SNTSyncConstants.h"
// https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/
static const NSUInteger kExpectedTeamIDLength = 10;
@interface SNTRule ()
@property(readwrite) NSUInteger timestamp;
@end
@@ -28,20 +35,84 @@
timestamp:(NSUInteger)timestamp {
self = [super init];
if (self) {
if (identifier.length == 0) {
return nil;
}
NSCharacterSet *nonHex =
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdef"] invertedSet];
NSCharacterSet *nonUppercaseAlphaNumeric = [[NSCharacterSet
characterSetWithCharactersInString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"] invertedSet];
switch (type) {
case SNTRuleTypeBinary: OS_FALLTHROUGH;
case SNTRuleTypeCertificate: {
// For binary and certificate rules, force the hash identifier to be lowercase hex.
identifier = [identifier lowercaseString];
identifier = [identifier stringByTrimmingCharactersInSet:nonHex];
if (identifier.length != (CC_SHA256_DIGEST_LENGTH * 2)) {
return nil;
}
break;
}
case SNTRuleTypeTeamID: {
// TeamIDs are always [0-9A-Z], so enforce that the identifier is uppercase
identifier =
[[identifier uppercaseString] stringByTrimmingCharactersInSet:nonUppercaseAlphaNumeric];
if (identifier.length != kExpectedTeamIDLength) {
return nil;
}
break;
}
case SNTRuleTypeSigningID: {
// SigningID rules are a combination of `TeamID:SigningID`. The TeamID should
// be forced to be uppercase, but because very loose rules exist for SigningIDs,
// their case will be kept as-is. However, platform binaries are expected to
// have the hardcoded string "platform" as the team ID and the case will be left
// as is.
NSArray *sidComponents = [identifier componentsSeparatedByString:@":"];
if (!sidComponents || sidComponents.count < 2) {
return nil;
}
// The first component is the TeamID
NSString *teamID = sidComponents[0];
if (![teamID isEqualToString:@"platform"]) {
teamID =
[[teamID uppercaseString] stringByTrimmingCharactersInSet:nonUppercaseAlphaNumeric];
if (teamID.length != kExpectedTeamIDLength) {
return nil;
}
}
// The rest of the components are the Signing ID since ":" a legal character.
// Join all but the last element of the components to rebuild the SigningID.
NSString *signingID = [[sidComponents
subarrayWithRange:NSMakeRange(1, sidComponents.count - 1)] componentsJoinedByString:@":"];
if (signingID.length == 0) {
return nil;
}
identifier = [NSString stringWithFormat:@"%@:%@", teamID, signingID];
break;
}
default: {
break;
}
}
_identifier = identifier;
_state = state;
_type = type;
_customMsg = customMsg;
_timestamp = timestamp;
if (_type == SNTRuleTypeBinary || _type == SNTRuleTypeCertificate) {
NSCharacterSet *nonHex =
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
if ([[_identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length != 64)
return nil;
} else if (_identifier.length == 0) {
return nil;
}
}
return self;
}
@@ -111,7 +182,14 @@
customMsg = nil;
}
return [self initWithIdentifier:identifier state:state type:type customMsg:customMsg];
NSString *customURL = dict[kRuleCustomURL];
if (![customURL isKindOfClass:[NSString class]] || customURL.length == 0) {
customURL = nil;
}
SNTRule *r = [self initWithIdentifier:identifier state:state type:type customMsg:customMsg];
r.customURL = customURL;
return r;
}
#pragma mark NSSecureCoding
@@ -131,6 +209,7 @@
ENCODE(@(self.state), @"state");
ENCODE(@(self.type), @"type");
ENCODE(self.customMsg, @"custommsg");
ENCODE(self.customURL, @"customurl");
ENCODE(@(self.timestamp), @"timestamp");
}
@@ -141,6 +220,7 @@
_state = [DECODE(NSNumber, @"state") intValue];
_type = [DECODE(NSNumber, @"type") intValue];
_customMsg = DECODE(NSString, @"custommsg");
_customURL = DECODE(NSString, @"customurl");
_timestamp = [DECODE(NSNumber, @"timestamp") unsignedIntegerValue];
}
return self;

View File

@@ -13,6 +13,7 @@
/// limitations under the License.
#import <XCTest/XCTest.h>
#include "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTRule.h"
@@ -46,13 +47,25 @@
XCTAssertEqual(sut.type, SNTRuleTypeCertificate);
XCTAssertEqual(sut.state, SNTRuleStateBlock);
// Ensure a Binary and Certificate rules properly convert identifiers to lowercase.
for (NSString *ruleType in @[ @"BINARY", @"CERTIFICATE" ]) {
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"B7C1E3FD640C5F211C89B02C2C6122F78CE322AA5C56EB0BB54BC422A8F8B670",
@"policy" : @"BLOCKLIST",
@"rule_type" : ruleType,
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
}
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"some-sort-of-identifier",
@"identifier" : @"ABCDEFGHIJ",
@"policy" : @"SILENT_BLOCKLIST",
@"rule_type" : @"TEAMID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(sut.type, SNTRuleTypeTeamID);
XCTAssertEqual(sut.state, SNTRuleStateSilentBlock);
@@ -68,26 +81,112 @@
XCTAssertEqual(sut.state, SNTRuleStateAllowCompiler);
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"some-sort-of-identifier",
@"identifier" : @"ABCDEFGHIJ",
@"policy" : @"REMOVE",
@"rule_type" : @"TEAMID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(sut.type, SNTRuleTypeTeamID);
XCTAssertEqual(sut.state, SNTRuleStateRemove);
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"some-sort-of-identifier",
@"identifier" : @"ABCDEFGHIJ",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"TEAMID",
@"custom_msg" : @"A custom block message",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(sut.type, SNTRuleTypeTeamID);
XCTAssertEqual(sut.state, SNTRuleStateAllow);
XCTAssertEqualObjects(sut.customMsg, @"A custom block message");
// TeamIDs must be 10 chars in length
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"A",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"TEAMID",
}];
XCTAssertNil(sut);
// TeamIDs must be only alphanumeric chars
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"ßßßßßßßßßß",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"TEAMID",
}];
XCTAssertNil(sut);
// TeamIDs are converted to uppercase
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"abcdefghij",
@"policy" : @"REMOVE",
@"rule_type" : @"TEAMID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
// SigningID tests
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"ABCDEFGHIJ:com.example",
@"policy" : @"REMOVE",
@"rule_type" : @"SIGNINGID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ:com.example");
XCTAssertEqual(sut.type, SNTRuleTypeSigningID);
XCTAssertEqual(sut.state, SNTRuleStateRemove);
// Invalid SingingID tests:
for (NSString *ident in @[
@":com.example", // missing team ID
@"ABCDEFGHIJ:", // missing signing ID
@"ABC:com.example", // Invalid team id
@":", // missing team and signing IDs
@"", // empty string
]) {
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : ident,
@"policy" : @"REMOVE",
@"rule_type" : @"SIGNINGID",
}];
XCTAssertNil(sut);
}
// Signing ID with lower team ID has case fixed up
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"abcdefghij:com.example",
@"policy" : @"REMOVE",
@"rule_type" : @"SIGNINGID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ:com.example");
// Signing ID with lower platform team ID is left alone
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"platform:com.example",
@"policy" : @"REMOVE",
@"rule_type" : @"SIGNINGID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"platform:com.example");
// Signing ID can contain the TID:SID delimiter character (":")
for (NSString *ident in @[
@"ABCDEFGHIJ:com:",
@"ABCDEFGHIJ:com:example",
@"ABCDEFGHIJ::",
@"ABCDEFGHIJ:com:example:with:more:components:",
]) {
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : ident,
@"policy" : @"ALLOWLIST",
@"rule_type" : @"SIGNINGID",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, ident);
}
}
- (void)testInitWithDictionaryInvalid {

View File

@@ -124,6 +124,7 @@ extern NSString *const kRuleTypeCertificate;
extern NSString *const kRuleTypeTeamID;
extern NSString *const kRuleTypeSigningID;
extern NSString *const kRuleCustomMsg;
extern NSString *const kRuleCustomURL;
extern NSString *const kCursor;
extern NSString *const kBackoffInterval;

View File

@@ -125,6 +125,7 @@ NSString *const kRuleTypeCertificate = @"CERTIFICATE";
NSString *const kRuleTypeTeamID = @"TEAMID";
NSString *const kRuleTypeSigningID = @"SIGNINGID";
NSString *const kRuleCustomMsg = @"custom_msg";
NSString *const kRuleCustomURL = @"custom_url";
NSString *const kCursor = @"cursor";
NSString *const kBackoffInterval = @"backoff";

View File

@@ -23,7 +23,9 @@
/// Protocol implemented by SantaGUI and utilized by santad
@protocol SNTNotifierXPC
- (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message;
- (void)postBlockNotification:(SNTStoredEvent *)event
withCustomMessage:(NSString *)message
andCustomURL:(NSString *)url;
- (void)postUSBBlockNotification:(SNTDeviceEvent *)event withCustomMessage:(NSString *)message;
- (void)postFileAccessBlockNotification:(SNTFileAccessEvent *)event
withCustomMessage:(NSString *)message API_AVAILABLE(macos(13.0));

View File

@@ -49,15 +49,18 @@
// function returns early due to interrupts.
void SleepMS(long ms);
enum class ActionType {
Auth,
Notify,
};
// Helper to construct strings of a given length
NSString *RepeatedString(NSString *str, NSUInteger len);
//
// Helpers to construct various ES structs
//
enum class ActionType {
Auth,
Notify,
};
audit_token_t MakeAuditToken(pid_t pid, pid_t pidver);
/// Construct a `struct stat` buffer with each member having a unique value.
@@ -67,7 +70,7 @@ audit_token_t MakeAuditToken(pid_t pid, pid_t pidver);
struct stat MakeStat(int offset = 0);
es_string_token_t MakeESStringToken(const char *s);
es_file_t MakeESFile(const char *path, struct stat sb = {});
es_file_t MakeESFile(const char *path, struct stat sb = MakeStat());
es_process_t MakeESProcess(es_file_t *file, audit_token_t tok = {}, audit_token_t parent_tok = {});
es_message_t MakeESMessage(es_event_type_t et, es_process_t *proc,
ActionType action_type = ActionType::Notify,

View File

@@ -21,6 +21,10 @@
#include <uuid/uuid.h>
#include "Source/common/SystemResources.h"
NSString *RepeatedString(NSString *str, NSUInteger len) {
return [@"" stringByPaddingToLength:len withString:str startingAtIndex:0];
}
audit_token_t MakeAuditToken(pid_t pid, pid_t pidver) {
return audit_token_t{
.val =

View File

@@ -23,7 +23,9 @@
///
@interface SNTBinaryMessageWindowController : SNTMessageWindowController
- (instancetype)initWithEvent:(SNTStoredEvent *)event andMessage:(NSString *)message;
- (instancetype)initWithEvent:(SNTStoredEvent *)event
customMsg:(NSString *)message
customURL:(NSString *)url;
- (IBAction)showCertInfo:(id)sender;
- (void)updateBlockNotification:(SNTStoredEvent *)event withBundleHash:(NSString *)bundleHash;

View File

@@ -25,6 +25,9 @@
/// The custom message to display for this event
@property(copy) NSString *customMessage;
/// The custom URL to use for this event
@property(copy) NSString *customURL;
/// A 'friendly' string representing the certificate information
@property(readonly, nonatomic) NSString *publisherInfo;
@@ -39,11 +42,14 @@
@implementation SNTBinaryMessageWindowController
- (instancetype)initWithEvent:(SNTStoredEvent *)event andMessage:(NSString *)message {
- (instancetype)initWithEvent:(SNTStoredEvent *)event
customMsg:(NSString *)message
customURL:(NSString *)url {
self = [super initWithWindowNibName:@"MessageWindow"];
if (self) {
_event = event;
_customMessage = message;
_customURL = url;
_progress = [NSProgress discreteProgressWithTotalUnitCount:1];
[_progress addObserver:self
forKeyPath:@"fractionCompleted"
@@ -74,7 +80,9 @@
- (void)loadWindow {
[super loadWindow];
if (![[SNTConfigurator configurator] eventDetailURL]) {
NSURL *url = [SNTBlockMessage eventDetailURLForEvent:self.event customURL:self.customURL];
if (!url) {
[self.openEventButton removeFromSuperview];
} else {
NSString *eventDetailText = [[SNTConfigurator configurator] eventDetailText];
@@ -120,7 +128,8 @@
}
- (IBAction)openEventDetails:(id)sender {
NSURL *url = [SNTBlockMessage eventDetailURLForEvent:self.event];
NSURL *url = [SNTBlockMessage eventDetailURLForEvent:self.event customURL:self.customURL];
[self closeWindow:sender];
[[NSWorkspace sharedWorkspace] openURL:url];
}

View File

@@ -172,7 +172,8 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
[dc postNotificationName:@"com.google.santa.notification.blockedeexecution"
object:@"com.google.santa"
userInfo:userInfo];
userInfo:userInfo
deliverImmediately:YES];
}
- (void)showQueuedWindow {
@@ -322,14 +323,16 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
[un addNotificationRequest:req withCompletionHandler:nil];
}
- (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message {
- (void)postBlockNotification:(SNTStoredEvent *)event
withCustomMessage:(NSString *)message
andCustomURL:(NSString *)url {
if (!event) {
LOGI(@"Error: Missing event object in message received from daemon!");
return;
}
SNTBinaryMessageWindowController *pendingMsg =
[[SNTBinaryMessageWindowController alloc] initWithEvent:event andMessage:message];
[[SNTBinaryMessageWindowController alloc] initWithEvent:event customMsg:message customURL:url];
[self queueMessage:pendingMsg];
}

View File

@@ -58,7 +58,7 @@
id dncMock = OCMClassMock([NSDistributedNotificationCenter class]);
OCMStub([dncMock defaultCenter]).andReturn(dncMock);
[sut postBlockNotification:ev withCustomMessage:@""];
[sut postBlockNotification:ev withCustomMessage:@"" andCustomURL:@""];
OCMVerify([dncMock postNotificationName:@"com.google.santa.notification.blockedeexecution"
object:@"com.google.santa"
@@ -68,7 +68,8 @@
XCTAssertEqualObjects(userInfo[@"ppid"], @1);
XCTAssertEqualObjects(userInfo[@"execution_time"], @1660221048);
return YES;
}]]);
}]
deliverImmediately:YES]);
}
@end

View File

@@ -359,6 +359,7 @@ objc_library(
":SNTDecisionCache",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
":TTYWriter",
":WatchItemPolicy",
":WatchItems",
"//Source/common:Platform",
@@ -373,6 +374,8 @@ objc_library(
"//Source/common:String",
"@MOLCertificate",
"@MOLCodesignChecker",
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/container:flat_hash_set",
],
)
@@ -692,6 +695,7 @@ objc_library(
":SNTExecutionController",
":SNTNotificationQueue",
":SNTSyncdQueue",
":TTYWriter",
":WatchItems",
"//Source/common:PrefixTree",
"//Source/common:SNTCommonEnums",

View File

@@ -25,7 +25,7 @@
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTRule.h"
static const uint32_t kRuleTableCurrentVersion = 5;
static const uint32_t kRuleTableCurrentVersion = 7;
// TODO(nguyenphillip): this should be configurable.
// How many rules must be in database before we start trying to remove transitive rules.
@@ -229,6 +229,28 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
newVersion = 5;
}
if (version < 6) {
// Force hash identifiers for Binary and Certificate rules to always be lowercase
[db executeUpdate:@"UPDATE rules SET identifier = LOWER(identifier) WHERE type = ? OR type = ?",
@(SNTRuleTypeBinary), @(SNTRuleTypeCertificate)];
// Force team ID identifiers for TeamID rules to always be uppercase
[db executeUpdate:@"UPDATE rules SET identifier = UPPER(identifier) WHERE type = ?",
@(SNTRuleTypeTeamID)];
// Note: Intentionally not attempting to migrate exsting SigningID rules to enforce
// the TeamID component to be uppercase. Since this is a newer rule type, it is
// assumed to be unnecessary and we'd rather not maintain the SQL to perform this
// migration automatically.
newVersion = 6;
}
if (version < 7) {
[db executeUpdate:@"ALTER TABLE 'rules' ADD 'customurl' TEXT"];
newVersion = 7;
}
// Save signing info for launchd and santad. Used to ensure they are always allowed.
self.santadCSInfo = [[MOLCodesignChecker alloc] initWithSelf];
self.launchdCSInfo = [[MOLCodesignChecker alloc] initWithPID:1];
@@ -292,11 +314,13 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
}
- (SNTRule *)ruleFromResultSet:(FMResultSet *)rs {
return [[SNTRule alloc] initWithIdentifier:[rs stringForColumn:@"identifier"]
state:[rs intForColumn:@"state"]
type:[rs intForColumn:@"type"]
customMsg:[rs stringForColumn:@"custommsg"]
timestamp:[rs intForColumn:@"timestamp"]];
SNTRule *r = [[SNTRule alloc] initWithIdentifier:[rs stringForColumn:@"identifier"]
state:[rs intForColumn:@"state"]
type:[rs intForColumn:@"type"]
customMsg:[rs stringForColumn:@"custommsg"]
timestamp:[rs intForColumn:@"timestamp"]];
r.customURL = [rs stringForColumn:@"customurl"];
return r;
}
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
@@ -410,10 +434,10 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
}
} else {
if (![db executeUpdate:@"INSERT OR REPLACE INTO rules "
@"(identifier, state, type, custommsg, timestamp) "
@"VALUES (?, ?, ?, ?, ?);",
@"(identifier, state, type, custommsg, customurl, timestamp) "
@"VALUES (?, ?, ?, ?, ?, ?);",
rule.identifier, @(rule.state), @(rule.type), rule.customMsg,
@(rule.timestamp)]) {
rule.customURL, @(rule.timestamp)]) {
[self fillError:error
code:SNTRuleTableErrorInsertOrReplaceFailed
message:[db lastErrorMessage]];

View File

@@ -36,7 +36,7 @@
- (SNTRule *)_exampleTeamIDRule {
SNTRule *r = [[SNTRule alloc] init];
r.identifier = @"teamID";
r.identifier = @"ABCDEFGHIJ";
r.state = SNTRuleStateBlock;
r.type = SNTRuleTypeTeamID;
r.customMsg = @"A teamID rule";
@@ -48,11 +48,11 @@
if (isPlatformBinary) {
r.identifier = @"platform:signingID";
} else {
r.identifier = @"teamID:signingID";
r.identifier = @"ABCDEFGHIJ:signingID";
}
r.state = SNTRuleStateBlock;
r.type = SNTRuleTypeSigningID;
r.customMsg = @"A teamID rule";
r.customMsg = @"A signingID rule";
return r;
}
@@ -187,9 +187,9 @@
SNTRule *r = [self.sut ruleForBinarySHA256:nil
signingID:nil
certificateSHA256:nil
teamID:@"teamID"];
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"teamID");
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(r.type, SNTRuleTypeTeamID);
XCTAssertEqual([self.sut teamIDRuleCount], 1);
@@ -211,12 +211,12 @@
XCTAssertEqual([self.sut signingIDRuleCount], 2);
SNTRule *r = [self.sut ruleForBinarySHA256:nil
signingID:@"teamID:signingID"
signingID:@"ABCDEFGHIJ:signingID"
certificateSHA256:nil
teamID:nil];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"teamID:signingID");
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ:signingID");
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
r = [self.sut ruleForBinarySHA256:nil
@@ -243,9 +243,9 @@
// See the comment in SNTRuleTable#ruleForBinarySHA256:certificateSHA256:teamID
SNTRule *r = [self.sut
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
signingID:@"teamID:signingID"
signingID:@"ABCDEFGHIJ:signingID"
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
teamID:@"teamID"];
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
@@ -253,9 +253,9 @@
r = [self.sut
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
signingID:@"teamID:signingID"
signingID:@"ABCDEFGHIJ:signingID"
certificateSHA256:@"unknowncert"
teamID:@"teamID"];
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
@@ -265,26 +265,26 @@
ruleForBinarySHA256:@"unknown"
signingID:@"unknown"
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
teamID:@"teamID"];
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier,
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
XCTAssertEqual(r.type, SNTRuleTypeCertificate, @"Implicit rule ordering failed");
r = [self.sut ruleForBinarySHA256:@"unknown"
signingID:@"teamID:signingID"
signingID:@"ABCDEFGHIJ:signingID"
certificateSHA256:@"unknown"
teamID:@"teamID"];
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"teamID:signingID");
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ:signingID");
XCTAssertEqual(r.type, SNTRuleTypeSigningID, @"Implicit rule ordering failed (SigningID)");
r = [self.sut ruleForBinarySHA256:@"unknown"
signingID:@"unknown"
certificateSHA256:@"unknown"
teamID:@"teamID"];
teamID:@"ABCDEFGHIJ"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"teamID");
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(r.type, SNTRuleTypeTeamID, @"Implicit rule ordering failed (TeamID)");
}

View File

@@ -34,6 +34,8 @@ static constexpr WatchItemPathType kWatchItemPolicyDefaultPathType =
static constexpr bool kWatchItemPolicyDefaultAllowReadAccess = false;
static constexpr bool kWatchItemPolicyDefaultAuditOnly = true;
static constexpr bool kWatchItemPolicyDefaultInvertProcessExceptions = false;
static constexpr bool kWatchItemPolicyDefaultEnableSilentMode = false;
static constexpr bool kWatchItemPolicyDefaultEnableSilentTTYMode = false;
struct WatchItemPolicy {
struct Process {
@@ -71,21 +73,29 @@ struct WatchItemPolicy {
bool ara = kWatchItemPolicyDefaultAllowReadAccess,
bool ao = kWatchItemPolicyDefaultAuditOnly,
bool ipe = kWatchItemPolicyDefaultInvertProcessExceptions,
std::vector<Process> procs = {})
bool esm = kWatchItemPolicyDefaultEnableSilentMode,
bool estm = kWatchItemPolicyDefaultEnableSilentTTYMode,
std::string_view cm = "", std::vector<Process> procs = {})
: name(n),
path(p),
path_type(pt),
allow_read_access(ara),
audit_only(ao),
invert_process_exceptions(ipe),
silent(esm),
silent_tty(estm),
custom_message(cm.length() == 0 ? std::nullopt
: std::make_optional<std::string>(cm)),
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;
}
@@ -99,10 +109,12 @@ struct WatchItemPolicy {
bool allow_read_access;
bool audit_only;
bool invert_process_exceptions;
bool silent;
bool silent_tty;
std::optional<std::string> custom_message;
std::vector<Process> processes;
// WIP - No current way to control via config
bool silent = true;
std::string version = "temp_version";
};

View File

@@ -40,6 +40,9 @@ extern NSString *const kWatchItemConfigKeyOptions;
extern NSString *const kWatchItemConfigKeyOptionsAllowReadAccess;
extern NSString *const kWatchItemConfigKeyOptionsAuditOnly;
extern NSString *const kWatchItemConfigKeyOptionsInvertProcessExceptions;
extern NSString *const kWatchItemConfigKeyOptionsEnableSilentMode;
extern NSString *const kWatchItemConfigKeyOptionsEnableSilentTTYMode;
extern NSString *const kWatchItemConfigKeyOptionsCustomMessage;
extern NSString *const kWatchItemConfigKeyProcesses;
extern NSString *const kWatchItemConfigKeyProcessesBinaryPath;
extern NSString *const kWatchItemConfigKeyProcessesCertificateSha256;

View File

@@ -54,6 +54,9 @@ NSString *const kWatchItemConfigKeyOptions = @"Options";
NSString *const kWatchItemConfigKeyOptionsAllowReadAccess = @"AllowReadAccess";
NSString *const kWatchItemConfigKeyOptionsAuditOnly = @"AuditOnly";
NSString *const kWatchItemConfigKeyOptionsInvertProcessExceptions = @"InvertProcessExceptions";
NSString *const kWatchItemConfigKeyOptionsEnableSilentMode = @"EnableSilentMode";
NSString *const kWatchItemConfigKeyOptionsEnableSilentTTYMode = @"EnableSilentTTYMode";
NSString *const kWatchItemConfigKeyOptionsCustomMessage = @"BlockMessage";
NSString *const kWatchItemConfigKeyProcesses = @"Processes";
NSString *const kWatchItemConfigKeyProcessesBinaryPath = @"BinaryPath";
NSString *const kWatchItemConfigKeyProcessesCertificateSha256 = @"CertificateSha256";
@@ -73,6 +76,10 @@ static constexpr NSUInteger kMaxSigningIDLength = 512;
// churn rebuilding glob paths based on the state of the filesystem.
static constexpr uint64_t kMinReapplyConfigFrequencySecs = 15;
// Semi-arbitrary max custom message length. The goal is to protect against
// potential unbounded lengths, but no real reason this cannot be higher.
static constexpr NSUInteger kWatchItemConfigOptionCustomMessageMaxLength = 2048;
namespace santa::santad::data_layer {
// Type aliases
@@ -126,6 +133,10 @@ static std::vector<uint8_t> HexStringToBytes(NSString *str) {
return bytes;
}
static inline bool GetBoolValue(NSDictionary *options, NSString *key, bool default_value) {
return options[key] ? [options[key] boolValue] : default_value;
}
// Given a length, returns a ValidatorBlock that confirms the
// string is a valid hex string of the given length.
ValidatorBlock HexValidator(NSUInteger expected_length) {
@@ -379,6 +390,12 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
/// <false/>
/// <key>InvertProcessExceptions</key>
/// <false/>
/// <key>EnableSilentMode</key>
/// <true/>
/// <key>EnableSilentTTYMode</key>
/// <true/>
/// <key>BlockMessage</key>
/// <string>...</string>
/// </dict>
/// <key>Processes</key>
/// <array>
@@ -405,31 +422,38 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
NSDictionary *options = watch_item[kWatchItemConfigKeyOptions];
if (options) {
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsAllowReadAccess, [NSNumber class],
err)) {
return false;
NSArray<NSString *> *boolOptions = @[
kWatchItemConfigKeyOptionsAllowReadAccess,
kWatchItemConfigKeyOptionsAuditOnly,
kWatchItemConfigKeyOptionsInvertProcessExceptions,
kWatchItemConfigKeyOptionsEnableSilentMode,
kWatchItemConfigKeyOptionsEnableSilentTTYMode,
];
for (NSString *key in boolOptions) {
if (!VerifyConfigKey(options, key, [NSNumber class], err)) {
return false;
}
}
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsAuditOnly, [NSNumber class], err)) {
return false;
}
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsInvertProcessExceptions,
[NSNumber class], err)) {
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsCustomMessage, [NSString class], err,
false,
LenRangeValidator(0, kWatchItemConfigOptionCustomMessageMaxLength))) {
return false;
}
}
bool allow_read_access = options[kWatchItemConfigKeyOptionsAllowReadAccess]
? [options[kWatchItemConfigKeyOptionsAllowReadAccess] boolValue]
: kWatchItemPolicyDefaultAllowReadAccess;
bool audit_only = options[kWatchItemConfigKeyOptionsAuditOnly]
? [options[kWatchItemConfigKeyOptionsAuditOnly] boolValue]
: kWatchItemPolicyDefaultAuditOnly;
bool allow_read_access = GetBoolValue(options, kWatchItemConfigKeyOptionsAllowReadAccess,
kWatchItemPolicyDefaultAllowReadAccess);
bool audit_only =
GetBoolValue(options, kWatchItemConfigKeyOptionsAuditOnly, kWatchItemPolicyDefaultAuditOnly);
bool invert_process_exceptions =
options[kWatchItemConfigKeyOptionsInvertProcessExceptions]
? [options[kWatchItemConfigKeyOptionsInvertProcessExceptions] boolValue]
: kWatchItemPolicyDefaultInvertProcessExceptions;
GetBoolValue(options, kWatchItemConfigKeyOptionsInvertProcessExceptions,
kWatchItemPolicyDefaultInvertProcessExceptions);
bool enable_silent_mode = GetBoolValue(options, kWatchItemConfigKeyOptionsEnableSilentMode,
kWatchItemPolicyDefaultEnableSilentMode);
bool enable_silent_tty_mode = GetBoolValue(options, kWatchItemConfigKeyOptionsEnableSilentTTYMode,
kWatchItemPolicyDefaultEnableSilentTTYMode);
std::variant<Unit, ProcessList> proc_list = VerifyConfigWatchItemProcesses(watch_item, err);
if (std::holds_alternative<Unit>(proc_list)) {
@@ -439,7 +463,10 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
for (const PathAndTypePair &path_type_pair : std::get<PathList>(path_list)) {
policies.push_back(std::make_shared<WatchItemPolicy>(
NSStringToUTF8StringView(name), path_type_pair.first, path_type_pair.second,
allow_read_access, audit_only, invert_process_exceptions, std::get<ProcessList>(proc_list)));
allow_read_access, audit_only, invert_process_exceptions, enable_silent_mode,
enable_silent_tty_mode,
NSStringToUTF8StringView(options[kWatchItemConfigKeyOptionsCustomMessage]),
std::get<ProcessList>(proc_list)));
}
return true;

View File

@@ -93,10 +93,6 @@ static NSMutableDictionary *WrapWatchItemsConfig(NSDictionary *config) {
return [@{@"Version" : @(kVersion.data()), @"WatchItems" : [config mutableCopy]} mutableCopy];
}
static NSString *RepeatedString(NSString *str, NSUInteger len) {
return [@"" stringByPaddingToLength:len withString:str startingAtIndex:0];
}
@interface WatchItemsTest : XCTestCase
@property NSFileManager *fileMgr;
@property NSString *testDir;
@@ -757,37 +753,66 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
&err));
// Options keys must be valid types
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAllowReadAccess : @""}
},
policies, &err));
XCTAssertTrue(ParseConfigSingleWatchItem(@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAllowReadAccess : @(0)}
},
policies, &err));
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAuditOnly : @""}
},
policies, &err));
XCTAssertTrue(ParseConfigSingleWatchItem(@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsAuditOnly : @(0)}
},
policies, &err));
{
// Check bool option keys
for (NSString *key in @[
kWatchItemConfigKeyOptionsAllowReadAccess,
kWatchItemConfigKeyOptionsAuditOnly,
kWatchItemConfigKeyOptionsInvertProcessExceptions,
kWatchItemConfigKeyOptionsEnableSilentMode,
kWatchItemConfigKeyOptionsEnableSilentTTYMode,
]) {
// Parse bool option with invliad type
XCTAssertFalse(ParseConfigSingleWatchItem(
@"",
@{kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{key : @""}},
policies, &err));
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsInvertProcessExceptions : @""}
},
policies, &err));
XCTAssertTrue(ParseConfigSingleWatchItem(@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsInvertProcessExceptions : @(0)}
},
policies, &err));
// Parse bool option with valid type
XCTAssertTrue(ParseConfigSingleWatchItem(
@"",
@{kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{key : @(0)}},
policies, &err));
}
// Check other option keys
// kWatchItemConfigKeyOptionsCustomMessage - Invalid type
XCTAssertFalse(ParseConfigSingleWatchItem(
@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsCustomMessage : @[]}
},
policies, &err));
// kWatchItemConfigKeyOptionsCustomMessage zero length
XCTAssertTrue(ParseConfigSingleWatchItem(
@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsCustomMessage : @""}
},
policies, &err));
// kWatchItemConfigKeyOptionsCustomMessage valid "normal" length
XCTAssertTrue(ParseConfigSingleWatchItem(
@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions :
@{kWatchItemConfigKeyOptionsCustomMessage : @"This is a custom message"}
},
policies, &err));
// kWatchItemConfigKeyOptionsCustomMessage Invalid "long" length
XCTAssertFalse(ParseConfigSingleWatchItem(
@"", @{
kWatchItemConfigKeyPaths : @[ @"a" ],
kWatchItemConfigKeyOptions :
@{kWatchItemConfigKeyOptionsCustomMessage : RepeatedString(@"A", 4096)}
},
policies, &err));
}
// If processes are specified, they must be valid format
// Note: Full tests in `testVerifyConfigWatchItemProcesses`
@@ -822,6 +847,9 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
kWatchItemConfigKeyOptionsAllowReadAccess : @(YES),
kWatchItemConfigKeyOptionsAuditOnly : @(NO),
kWatchItemConfigKeyOptionsInvertProcessExceptions : @(YES),
kWatchItemConfigKeyOptionsEnableSilentMode : @(YES),
kWatchItemConfigKeyOptionsEnableSilentMode : @(NO),
kWatchItemConfigKeyOptionsCustomMessage : @"",
},
kWatchItemConfigKeyProcesses : @[
@{kWatchItemConfigKeyProcessesBinaryPath : @"pa"},
@@ -829,11 +857,12 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
]
},
policies, &err));
XCTAssertEqual(policies.size(), 2);
XCTAssertEqual(*policies[0].get(), WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType,
true, false, true, procs));
true, false, true, true, false, "", procs));
XCTAssertEqual(*policies[1].get(), WatchItemPolicy("rule", "b", WatchItemPathType::kPrefix, true,
false, true, procs));
false, true, true, false, "", procs));
}
- (void)testState {

View File

@@ -60,7 +60,8 @@ NSString *const FlushCacheReasonToString(FlushCacheReason reason) {
case FlushCacheReason::kExplicitCommand: return kFlushCacheReasonExplicitCommand;
case FlushCacheReason::kFilesystemUnmounted: return kFlushCacheReasonFilesystemUnmounted;
default:
[NSException raise:@"Invalid reason" format:@"Unknown reason value: %d", reason];
[NSException raise:@"Invalid reason"
format:@"Unknown reason value: %d", static_cast<int>(reason)];
return nil;
}
}

View File

@@ -25,6 +25,7 @@
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
#include "Source/santad/Metrics.h"
#import "Source/santad/SNTDecisionCache.h"
#include "Source/santad/TTYWriter.h"
typedef void (^SNTFileAccessBlockCallback)(SNTFileAccessEvent *event);
@@ -39,7 +40,8 @@ typedef void (^SNTFileAccessBlockCallback)(SNTFileAccessEvent *event);
watchItems:(std::shared_ptr<santa::santad::data_layer::WatchItems>)watchItems
enricher:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
decisionCache:(SNTDecisionCache *)decisionCache;
decisionCache:(SNTDecisionCache *)decisionCache
ttyWriter:(std::shared_ptr<santa::santad::TTYWriter>)ttyWriter;
@property SNTFileAccessBlockCallback fileAccessBlockCallback;

View File

@@ -20,14 +20,15 @@
#import <MOLCodesignChecker/MOLCodesignChecker.h>
#include <bsm/libbsm.h>
#include <sys/fcntl.h>
#include <sys/types.h>
#include <algorithm>
#include <array>
#include <cstdlib>
#include <memory>
#include <optional>
#include <set>
#include <type_traits>
#include <utility>
#include <variant>
#include "Source/common/Platform.h"
@@ -45,9 +46,12 @@
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/RateLimiter.h"
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
using santa::common::StringToNSString;
using santa::santad::EventDisposition;
using santa::santad::TTYWriter;
using santa::santad::data_layer::WatchItemPathType;
using santa::santad::data_layer::WatchItemPolicy;
using santa::santad::data_layer::WatchItems;
@@ -69,6 +73,106 @@ static constexpr uint16_t kDefaultRateLimitQPS = 50;
struct PathTarget {
std::string path;
bool isReadable;
std::optional<std::pair<dev_t, ino_t>> devnoIno;
};
// This is a bespoke cache for mapping processes to a set of 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.
// 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>>;
public:
ProcessFiles() {
q_ = dispatch_queue_create(
"com.google.santa.daemon.faa",
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
QOS_CLASS_USER_INTERACTIVE, 0));
};
// Add the given target to the set of files a process can read
void Set(const es_process_t *proc, const PathTarget &target) {
if (!target.devnoIno.has_value()) {
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);
});
}
// Remove the given process from the cache
void Remove(const es_process_t *proc) {
std::pair<pid_t, pid_t> pidPidver = {audit_token_to_pid(proc->audit_token),
audit_token_to_pidversion(proc->audit_token)};
dispatch_sync(q_, ^{
cache_.erase(pidPidver);
});
}
// Check if the set of files for a given process contains the given file
bool Exists(const es_process_t *proc, 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};
__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;
}
// Clear all cache entries
void Clear() {
dispatch_sync(q_, ^{
ClearLocked();
});
}
private:
// Remove everything in the cache.
void ClearLocked() { cache_.clear(); }
dispatch_queue_t q_;
absl::flat_hash_map<std::pair<pid_t, pid_t>, FileSet> cache_;
// Cache limits are merely meant to protect against unbounded growth. In practice,
// the observed cache size is typically small for normal WatchItems rules (those
// that do not target high-volume paths). The per entry size was observed to vary
// quite dramatically based on the type of process (e.g. large, complex applications
// were observed to frequently have several thousands of entries).
static constexpr size_t kMaxCacheSize = 512;
static constexpr size_t kMaxCacheEntrySize = 8192;
};
static inline std::string Path(const es_file_t *esFile) {
@@ -82,14 +186,18 @@ static inline std::string Path(const es_string_token_t &tok) {
static inline void PushBackIfNotTruncated(std::vector<PathTarget> &vec, const es_file_t *esFile,
bool isReadable = false) {
if (!esFile->path_truncated) {
vec.push_back({Path(esFile), isReadable});
vec.push_back({Path(esFile), isReadable,
isReadable ? std::make_optional<std::pair<dev_t, ino_t>>(
{esFile->stat.st_dev, esFile->stat.st_ino})
: std::nullopt});
}
}
// Note: This variant of PushBackIfNotTruncated can never be marked "isReadable"
static inline void PushBackIfNotTruncated(std::vector<PathTarget> &vec, const es_file_t *dir,
const es_string_token_t &name, bool isReadable = false) {
const es_string_token_t &name) {
if (!dir->path_truncated) {
vec.push_back({Path(dir) + "/" + Path(name), isReadable});
vec.push_back({Path(dir) + "/" + Path(name), false, std::nullopt});
}
}
@@ -103,9 +211,9 @@ es_auth_result_t FileAccessPolicyDecisionToESAuthResult(FileAccessPolicyDecision
case FileAccessPolicyDecision::kAllowedAuditOnly: return ES_AUTH_RESULT_ALLOW;
default:
// This is a programming error. Bail.
LOGE(@"Invalid file access decision encountered: %d", decision);
LOGE(@"Invalid file access decision encountered: %d", static_cast<int>(decision));
[NSException raise:@"Invalid FileAccessPolicyDecision"
format:@"Invalid FileAccessPolicyDecision: %d", decision];
format:@"Invalid FileAccessPolicyDecision: %d", static_cast<int>(decision)];
}
}
@@ -118,6 +226,10 @@ bool ShouldLogDecision(FileAccessPolicyDecision decision) {
}
}
bool ShouldNotifyUserDecision(FileAccessPolicyDecision decision) {
return ShouldLogDecision(decision) && decision != FileAccessPolicyDecision::kAllowedAuditOnly;
}
es_auth_result_t CombinePolicyResults(es_auth_result_t result1, es_auth_result_t result2) {
// If either policy denied the operation, the operation is denied
return ((result1 == ES_AUTH_RESULT_DENY || result2 == ES_AUTH_RESULT_DENY)
@@ -207,6 +319,8 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
std::shared_ptr<Enricher> _enricher;
std::shared_ptr<RateLimiter> _rateLimiter;
SantaCache<SantaVnode, NSString *> _certHashCache;
std::shared_ptr<TTYWriter> _ttyWriter;
ProcessFiles _readsCache;
}
- (instancetype)
@@ -217,7 +331,8 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
watchItems:(std::shared_ptr<WatchItems>)watchItems
enricher:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
decisionCache:(SNTDecisionCache *)decisionCache {
decisionCache:(SNTDecisionCache *)decisionCache
ttyWriter:(std::shared_ptr<santa::santad::TTYWriter>)ttyWriter {
self = [super initWithESAPI:std::move(esApi)
metrics:metrics
processor:santa::santad::Processor::kFileAccessAuthorizer];
@@ -225,8 +340,8 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
_watchItems = std::move(watchItems);
_logger = std::move(logger);
_enricher = std::move(enricher);
_decisionCache = decisionCache;
_ttyWriter = std::move(ttyWriter);
_rateLimiter = RateLimiter::Create(metrics, santa::santad::Processor::kFileAccessAuthorizer,
kDefaultRateLimitQPS);
@@ -420,6 +535,11 @@ 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);
}
// Check if this action contains any special case that would produce
// an immediate result.
FileAccessPolicyDecision specialCase = [self specialCaseForPolicy:policy
@@ -485,7 +605,7 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
targetPathCopy, policyDecision);
}];
}
#if 0
if (!optionalPolicy.value()->silent && self.fileAccessBlockCallback) {
SNTCachedDecision *cd =
[self.decisionCache cachedDecisionForFile:msg->process->executable->stat];
@@ -506,6 +626,7 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
self.fileAccessBlockCallback(event);
}
#endif
}
return policyDecision;
@@ -558,6 +679,18 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
- (void)handleMessage:(santa::santad::event_providers::endpoint_security::Message &&)esMsg
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
if (esMsg->event_type == ES_EVENT_TYPE_AUTH_OPEN &&
!(esMsg->event.open.fflag & kOpenFlagsIndicatingWrite)) {
if (self->_readsCache.Exists(esMsg->process, esMsg->event.open.file)) {
[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);
return;
}
[self processMessage:std::move(esMsg)
handler:^(const Message &msg) {
[self processMessage:msg];
@@ -566,11 +699,10 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
}
- (void)enable {
// TODO(xyz): Expand to support ES_EVENT_TYPE_AUTH_CREATE, ES_EVENT_TYPE_AUTH_TRUNCATE
std::set<es_event_type_t> events = {
ES_EVENT_TYPE_AUTH_CLONE, ES_EVENT_TYPE_AUTH_CREATE, ES_EVENT_TYPE_AUTH_EXCHANGEDATA,
ES_EVENT_TYPE_AUTH_LINK, ES_EVENT_TYPE_AUTH_OPEN, ES_EVENT_TYPE_AUTH_RENAME,
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK,
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK, ES_EVENT_TYPE_NOTIFY_EXIT,
};
#if HAVE_MACOS_12
@@ -614,6 +746,8 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
// begin receiving events (if not already)
[self enable];
}
self->_readsCache.Clear();
}
@end

View File

@@ -20,6 +20,7 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <sys/fcntl.h>
#include <sys/types.h>
#include <cstring>
#include <array>
@@ -50,14 +51,20 @@ extern NSString *kBadCertHash;
struct PathTarget {
std::string path;
bool isReadable;
std::optional<std::pair<dev_t, ino_t>> devnoIno;
};
using PathTargetsPair = std::pair<std::optional<std::string>, std::optional<std::string>>;
extern void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets);
extern es_auth_result_t FileAccessPolicyDecisionToESAuthResult(FileAccessPolicyDecision decision);
extern bool ShouldLogDecision(FileAccessPolicyDecision decision);
extern bool ShouldNotifyUserDecision(FileAccessPolicyDecision decision);
extern es_auth_result_t CombinePolicyResults(es_auth_result_t result1, es_auth_result_t result2);
static inline std::pair<dev_t, ino_t> FileID(const es_file_t &file) {
return std::make_pair(file.stat.st_dev, file.stat.st_ino);
}
void SetExpectationsForFileAccessAuthorizerInit(
std::shared_ptr<MockEndpointSecurityAPI> mockESApi) {
EXPECT_CALL(*mockESApi, InvertTargetPathMuting).WillOnce(testing::Return(true));
@@ -135,7 +142,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
logger:nullptr
watchItems:nullptr
enricher:nullptr
decisionCache:self.dcMock];
decisionCache:self.dcMock
ttyWriter:nullptr];
//
// Test 1 - Not in local cache or decision cache, and code sig lookup fails
@@ -229,7 +237,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
{FileAccessPolicyDecision::kAllowed, false},
{FileAccessPolicyDecision::kAllowedReadAccess, false},
{FileAccessPolicyDecision::kAllowedAuditOnly, true},
{(FileAccessPolicyDecision)5, false},
{(FileAccessPolicyDecision)123, false},
};
for (const auto &kv : policyDecisionToShouldLog) {
@@ -237,6 +245,22 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
}
}
- (void)testShouldNotifyUserDecision {
std::map<FileAccessPolicyDecision, bool> policyDecisionToShouldLog = {
{FileAccessPolicyDecision::kNoPolicy, false},
{FileAccessPolicyDecision::kDenied, true},
{FileAccessPolicyDecision::kDeniedInvalidSignature, true},
{FileAccessPolicyDecision::kAllowed, false},
{FileAccessPolicyDecision::kAllowedReadAccess, false},
{FileAccessPolicyDecision::kAllowedAuditOnly, false},
{(FileAccessPolicyDecision)123, false},
};
for (const auto &kv : policyDecisionToShouldLog) {
XCTAssertEqual(ShouldNotifyUserDecision(kv.first), kv.second);
}
}
- (void)testCombinePolicyResults {
// Ensure that the combined result is ES_AUTH_RESULT_DENY if both or either
// input result is ES_AUTH_RESULT_DENY.
@@ -269,7 +293,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
logger:nullptr
watchItems:nullptr
enricher:nullptr
decisionCache:nil];
decisionCache:nil
ttyWriter:nullptr];
auto policy = std::make_shared<WatchItemPolicy>("foo_policy", "/foo");
@@ -397,7 +422,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
logger:nullptr
watchItems:nullptr
enricher:nullptr
decisionCache:nil];
decisionCache:nil
ttyWriter:nullptr];
id accessClientMock = OCMPartialMock(accessClient);
@@ -515,7 +541,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
logger:nullptr
watchItems:nullptr
enricher:nullptr
decisionCache:nil];
decisionCache:nil
ttyWriter:nullptr];
id accessClientMock = OCMPartialMock(accessClient);
@@ -642,7 +669,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
std::set<es_event_type_t> expectedEventSubs = {
ES_EVENT_TYPE_AUTH_CLONE, ES_EVENT_TYPE_AUTH_CREATE, ES_EVENT_TYPE_AUTH_EXCHANGEDATA,
ES_EVENT_TYPE_AUTH_LINK, ES_EVENT_TYPE_AUTH_OPEN, ES_EVENT_TYPE_AUTH_RENAME,
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK,
ES_EVENT_TYPE_AUTH_TRUNCATE, ES_EVENT_TYPE_AUTH_UNLINK, ES_EVENT_TYPE_NOTIFY_EXIT,
};
#if HAVE_MACOS_12
@@ -678,7 +705,8 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
logger:nullptr
watchItems:nullptr
enricher:nullptr
decisionCache:nil];
decisionCache:nil
ttyWriter:nullptr];
EXPECT_CALL(*mockESApi, UnsubscribeAll);
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).WillOnce(testing::Return(true));
@@ -692,9 +720,9 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
- (void)testGetPathTargets {
// This test ensures that the `GetPathTargets` functions returns the
// expected combination of targets for each handled event variant
es_file_t testFile1 = MakeESFile("test_file_1");
es_file_t testFile2 = MakeESFile("test_file_2");
es_file_t testDir = MakeESFile("test_dir");
es_file_t testFile1 = MakeESFile("test_file_1", MakeStat(100));
es_file_t testFile2 = MakeESFile("test_file_2", MakeStat(200));
es_file_t testDir = MakeESFile("test_dir", MakeStat(300));
es_string_token_t testTok = MakeESStringToken("test_tok");
std::string dirTok = std::string(testDir.path.data) + "/" + std::string(testTok.data);
@@ -715,6 +743,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 1);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertTrue(targets[0].isReadable);
XCTAssertEqual(targets[0].devnoIno.value(), FileID(testFile1));
}
{
@@ -729,8 +758,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
XCTAssertCppStringEqual(targets[1].path, dirTok);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
{
@@ -747,8 +778,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
{
@@ -762,8 +795,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
XCTAssertCppStringEqual(targets[1].path, dirTok);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
}
@@ -777,6 +812,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 1);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
}
{
@@ -791,8 +827,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertTrue(targets[0].isReadable);
XCTAssertEqual(targets[0].devnoIno.value(), FileID(testFile1));
XCTAssertCppStringEqual(targets[1].path, dirTok);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
{
@@ -806,8 +844,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
{
@@ -822,6 +862,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 1);
XCTAssertCppStringEqual(targets[0].path, dirTok);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
}
{
@@ -834,6 +875,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 1);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].isReadable);
XCTAssertFalse(targets[0].devnoIno.has_value());
}
if (@available(macOS 12.0, *)) {
@@ -852,8 +894,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertTrue(targets[0].isReadable);
XCTAssertEqual(targets[0].devnoIno.value(), FileID(testFile1));
XCTAssertCppStringEqual(targets[1].path, dirTok);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
{
@@ -865,8 +909,10 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertTrue(targets[0].isReadable);
XCTAssertEqual(targets[0].devnoIno.value(), FileID(testFile1));
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
XCTAssertFalse(targets[1].isReadable);
XCTAssertFalse(targets[1].devnoIno.has_value());
}
}
}

View File

@@ -69,6 +69,7 @@ santa_unit_test(
deps = [
":fsspool",
":fsspool_log_batch_writer",
"//Source/common:TestUtils",
"@OCMock",
],
)

View File

@@ -16,6 +16,7 @@
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
#include <functional>
#include <limits>
@@ -156,6 +157,12 @@ std::string SpoolDirectory(absl::string_view base_dir) {
return absl::StrCat(base_dir, PathSeparator(), kSpoolDirName);
}
bool operator==(struct timespec a, struct timespec b) {
return a.tv_sec == b.tv_sec && a.tv_nsec == b.tv_nsec;
}
bool operator!=(struct timespec a, struct timespec b) { return !(a == b); }
} // namespace
FsSpoolWriter::FsSpoolWriter(absl::string_view base_dir, size_t max_spool_size)
@@ -197,6 +204,25 @@ std::string FsSpoolWriter::UniqueFilename() {
return result;
}
absl::StatusOr<size_t> FsSpoolWriter::EstimateSpoolDirSize() {
struct stat stats;
if (stat(spool_dir_.c_str(), &stats) < 0) {
return absl::ErrnoToStatus(errno, "failed to stat spool directory");
}
if (stats.st_mtimespec != spool_dir_last_mtime_) {
// Store the updated mtime
spool_dir_last_mtime_ = stats.st_mtimespec;
// Recompute the current estimated size
return EstimateDirSize(spool_dir_);
} else {
// If the spool's last modification time hasn't changed then
// re-use the current estimate.
return spool_size_estimate_;
}
}
absl::Status FsSpoolWriter::WriteMessage(absl::string_view msg) {
if (absl::Status status = BuildDirectoryStructureIfNeeded(); !status.ok()) {
return status; // << "can't create directory structure for writer";
@@ -209,7 +235,7 @@ absl::Status FsSpoolWriter::WriteMessage(absl::string_view msg) {
// Recompute the spool size if we think we are
// over the limit.
if (spool_size_estimate_ > max_spool_size_) {
absl::StatusOr<size_t> estimate = EstimateDirSize(spool_dir_);
absl::StatusOr<size_t> estimate = EstimateSpoolDirSize();
if (!estimate.ok()) {
return estimate.status(); // failed to recompute spool size
}

View File

@@ -25,6 +25,11 @@
#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"
// Forward declarations
namespace fsspool {
class FsSpoolWriterPeer;
}
namespace fsspool {
// Enqueues messages into the spool. Multiple concurrent writers can
@@ -42,10 +47,13 @@ class FsSpoolWriter {
// returns the UNAVAILABLE canonical code (which is retryable).
absl::Status WriteMessage(absl::string_view msg);
friend class fsspool::FsSpoolWriterPeer;
private:
const std::string base_dir_;
const std::string spool_dir_;
const std::string tmp_dir_;
struct timespec spool_dir_last_mtime_;
// Approximate maximum size of the spooling area, in bytes. If a message is
// being written to a spooling area which already contains more than
@@ -81,6 +89,10 @@ class FsSpoolWriter {
// Generates a unique filename by combining the random ID of
// this writer with a sequence number.
std::string UniqueFilename();
// Estimate the size of the spool directory. However, only recompute a new
// estimate if the spool directory has has a change to its modification time.
absl::StatusOr<size_t> EstimateSpoolDirSize();
};
// This class is thread-unsafe.

View File

@@ -33,7 +33,6 @@ FsSpoolLogBatchWriter::~FsSpoolLogBatchWriter() {
if (!s.ok()) {
os_log(OS_LOG_DEFAULT, "Flush() failed with %s",
s.ToString(absl::StatusToStringMode::kWithEverything).c_str());
// LOG(WARNING) << "Flush() failed with " << s;
}
}

View File

@@ -18,13 +18,31 @@
#include <memory>
#include "Source/common/TestUtils.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool_log_batch_writer.h"
#include "google/protobuf/any.pb.h"
#include "google/protobuf/timestamp.pb.h"
namespace fsspool {
class FsSpoolWriterPeer : public FsSpoolWriter {
public:
// Constructors
using FsSpoolWriter::FsSpoolWriter;
// Private Methods
using FsSpoolWriter::BuildDirectoryStructureIfNeeded;
using FsSpoolWriter::EstimateSpoolDirSize;
// Private member variables
using FsSpoolWriter::spool_size_estimate_;
};
} // namespace fsspool
using fsspool::FsSpoolLogBatchWriter;
using fsspool::FsSpoolWriter;
using fsspool::FsSpoolWriterPeer;
static constexpr size_t kSpoolSize = 1048576;
@@ -72,8 +90,65 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
XCTAssertTrue([self.fileMgr removeItemAtPath:self.testDir error:nil]);
}
- (void)testEstimateSpoolDirSize {
NSString *testData = @"What a day for some testing!";
NSString *largeTestData = RepeatedString(@"A", 10240);
NSString *path = [NSString stringWithFormat:@"%@/%@", self.spoolDir, @"temppy.log"];
NSString *emptyPath = [NSString stringWithFormat:@"%@/%@", self.spoolDir, @"empty.log"];
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
// Create the spool dir structure and ensure no files exist
XCTAssertStatusOk(writer->BuildDirectoryStructureIfNeeded());
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:nil] count], 0);
// Ensure that the initial spool dir estimate is 0
auto status = writer->EstimateSpoolDirSize();
XCTAssertStatusOk(status);
XCTAssertEqual(*status, 0);
// Force the current estimate to be 0 since we're not recomputing on first write.
writer->spool_size_estimate_ = *status;
XCTAssertTrue([testData writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil]);
// Ensure the test file was created
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:nil] count], 1);
// Ensure the spool size estimate has grown at least as much as the content length
status = writer->EstimateSpoolDirSize();
XCTAssertStatusOk(status);
// Update the current estimate
writer->spool_size_estimate_ = *status;
XCTAssertGreaterThanOrEqual(writer->spool_size_estimate_, testData.length);
// Modify file contents without modifying spool directory mtime
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:path];
[fileHandle seekToEndOfFile];
[fileHandle writeData:[largeTestData dataUsingEncoding:NSUTF8StringEncoding]];
[fileHandle closeFile];
// Ensure only one file still exists
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:nil] count], 1);
// Ensure that the returned estimate is the same as the old since mtime didn't change
status = writer->EstimateSpoolDirSize();
XCTAssertStatusOk(status);
// Check that the current estimate is the same as the old estimate
XCTAssertEqual(*status, writer->spool_size_estimate_);
// Create a second file in the spool dir to bump mtime
XCTAssertTrue([@"" writeToFile:emptyPath atomically:YES encoding:NSUTF8StringEncoding error:nil]);
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:nil] count], 2);
status = writer->EstimateSpoolDirSize();
XCTAssertStatusOk(status);
// Ensure the newly returned size is appropriate
XCTAssertGreaterThanOrEqual(*status, testData.length + largeTestData.length);
}
- (void)testSimpleWrite {
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.baseDir]);
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.spoolDir]);
@@ -90,7 +165,7 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
}
- (void)testSpoolFull {
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
const std::string largeMessage(kSpoolSize + 1, '\x42');
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.baseDir]);
@@ -121,7 +196,7 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
}
- (void)testWriteMessageNoFlush {
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
FsSpoolLogBatchWriter batch_writer(writer.get(), 10);
// Ensure that writing in batch mode doesn't flsuh on individual writes.
@@ -134,7 +209,7 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
- (void)testWriteMessageFlushAtCapacity {
static const int kCapacity = 5;
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
FsSpoolLogBatchWriter batch_writer(writer.get(), kCapacity);
// Ensure batch flushed once capacity exceeded
@@ -153,7 +228,7 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
static const int kCapacity = 5;
static const int kExpectedFlushes = 3;
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
FsSpoolLogBatchWriter batch_writer(writer.get(), kCapacity);
// Ensure batch flushed expected number of times
@@ -173,7 +248,7 @@ google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
static const int kCapacity = 10;
static const int kNumberOfWrites = 7;
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
auto writer = std::make_unique<FsSpoolWriterPeer>([self.baseDir UTF8String], kSpoolSize);
{
// Extra scope to enforce early destroy of batch_writer.

View File

@@ -63,7 +63,8 @@ NSString *const ProcessorToString(Processor processor) {
case Processor::kTamperResistance: return kProcessorTamperResistance;
case Processor::kFileAccessAuthorizer: return kProcessorFileAccessAuthorizer;
default:
[NSException raise:@"Invalid processor" format:@"Unknown processor value: %d", processor];
[NSException raise:@"Invalid processor"
format:@"Unknown processor value: %d", static_cast<int>(processor)];
return nil;
}
}
@@ -103,7 +104,8 @@ NSString *const EventDispositionToString(EventDisposition d) {
case EventDisposition::kDropped: return kEventDispositionDropped;
case EventDisposition::kProcessed: return kEventDispositionProcessed;
default:
[NSException raise:@"Invalid disposition" format:@"Unknown disposition value: %d", d];
[NSException raise:@"Invalid disposition"
format:@"Unknown disposition value: %d", static_cast<int>(d)];
return nil;
}
}

View File

@@ -85,8 +85,8 @@ SNTCachedDecision *MakeCachedDecision(struct stat sb, SNTEventState decision) {
OCMExpect([self.mockRuleDatabase
resetTimestampForRule:[OCMArg checkWithBlock:^BOOL(SNTRule *rule) {
return rule.identifier == cd.sha256 && rule.state == SNTRuleStateAllowTransitive &&
rule.type == SNTRuleTypeBinary;
return [rule.identifier isEqualToString:cd.sha256] &&
rule.state == SNTRuleStateAllowTransitive && rule.type == SNTRuleTypeBinary;
}]]);
[dc resetTimestampForCachedDecision:sb];

View File

@@ -312,7 +312,7 @@ static NSString *const kPrinterProxyPostMonterey =
@"\033[1mIdentifier:\033[0m %@\n"
@"\033[1mParent: \033[0m %@ (%@)\n\n",
se.filePath, se.fileSHA256, se.parentName, se.ppid];
NSURL *detailURL = [SNTBlockMessage eventDetailURLForEvent:se];
NSURL *detailURL = [SNTBlockMessage eventDetailURLForEvent:se customURL:cd.customURL];
if (detailURL) {
[msg appendFormat:@"More info:\n%@\n\n", detailURL.absoluteString];
}
@@ -320,7 +320,7 @@ static NSString *const kPrinterProxyPostMonterey =
self->_ttyWriter->Write(targetProc->tty->path.data, msg);
}
[self.notifierQueue addEvent:se customMessage:cd.customMsg];
[self.notifierQueue addEvent:se withCustomMessage:cd.customMsg andCustomURL:cd.customURL];
}
}
}

View File

@@ -21,6 +21,8 @@
@property(nonatomic) MOLXPCConnection *notifierConnection;
- (void)addEvent:(SNTStoredEvent *)event customMessage:(NSString *)message;
- (void)addEvent:(SNTStoredEvent *)event
withCustomMessage:(NSString *)message
andCustomURL:(NSString *)url;
@end

View File

@@ -36,18 +36,21 @@ static const int kMaximumNotifications = 10;
return self;
}
- (void)addEvent:(SNTStoredEvent *)event customMessage:(NSString *)message {
- (void)addEvent:(SNTStoredEvent *)event
withCustomMessage:(NSString *)message
andCustomURL:(NSString *)url {
if (!event) return;
if (self.pendingNotifications.count > kMaximumNotifications) {
LOGI(@"Pending GUI notification count is over %d, dropping.", kMaximumNotifications);
return;
}
NSDictionary *d;
NSMutableDictionary *d = [@{@"event" : event} mutableCopy];
if (message) {
d = @{@"event" : event, @"message" : message};
} else {
d = @{@"event" : event};
d[@"message"] = message;
}
if (url) {
d[@"url"] = url;
}
@synchronized(self.pendingNotifications) {
[self.pendingNotifications addObject:d];
@@ -62,7 +65,9 @@ static const int kMaximumNotifications = 10;
@synchronized(self.pendingNotifications) {
NSMutableArray *postedNotifications = [NSMutableArray array];
for (NSDictionary *d in self.pendingNotifications) {
[rop postBlockNotification:d[@"event"] withCustomMessage:d[@"message"]];
[rop postBlockNotification:d[@"event"]
withCustomMessage:d[@"message"]
andCustomURL:d[@"url"]];
[postedNotifications addObject:d];
}
[self.pendingNotifications removeObjectsInArray:postedNotifications];

View File

@@ -103,6 +103,7 @@
case SNTRuleStateSilentBlock: cd.silentBlock = YES;
case SNTRuleStateBlock:
cd.customMsg = rule.customMsg;
cd.customURL = rule.customURL;
cd.decision = SNTEventStateBlockBinary;
return cd;
case SNTRuleStateAllowCompiler:
@@ -136,6 +137,7 @@
// intentional fallthrough
case SNTRuleStateBlock:
cd.customMsg = rule.customMsg;
cd.customURL = rule.customURL;
cd.decision = SNTEventStateBlockSigningID;
return cd;
default: break;
@@ -149,6 +151,7 @@
// intentional fallthrough
case SNTRuleStateBlock:
cd.customMsg = rule.customMsg;
cd.customURL = rule.customURL;
cd.decision = SNTEventStateBlockCertificate;
return cd;
default: break;
@@ -162,6 +165,7 @@
// intentional fallthrough
case SNTRuleStateBlock:
cd.customMsg = rule.customMsg;
cd.customURL = rule.customURL;
cd.decision = SNTEventStateBlockTeamID;
return cd;
default: break;

View File

@@ -29,6 +29,7 @@
#import "Source/santad/SNTExecutionController.h"
#import "Source/santad/SNTNotificationQueue.h"
#import "Source/santad/SNTSyncdQueue.h"
#include "Source/santad/TTYWriter.h"
void SantadMain(
std::shared_ptr<
@@ -45,7 +46,7 @@ void SantadMain(
SNTCompilerController* compiler_controller,
SNTNotificationQueue* notifier_queue, SNTSyncdQueue* syncd_queue,
SNTExecutionController* exec_controller,
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>>
prefix_tree);
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> prefix_tree,
std::shared_ptr<santa::santad::TTYWriter> tty_writer);
#endif

View File

@@ -37,10 +37,12 @@
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
#include "Source/santad/SNTDaemonControlController.h"
#include "Source/santad/SNTDecisionCache.h"
#include "Source/santad/TTYWriter.h"
using santa::common::PrefixTree;
using santa::common::Unit;
using santa::santad::Metrics;
using santa::santad::TTYWriter;
using santa::santad::data_layer::WatchItems;
using santa::santad::event_providers::AuthResultCache;
using santa::santad::event_providers::FlushCacheMode;
@@ -79,7 +81,8 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
MOLXPCConnection *control_connection, SNTCompilerController *compiler_controller,
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
SNTExecutionController *exec_controller,
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> prefix_tree) {
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> prefix_tree,
std::shared_ptr<TTYWriter> tty_writer) {
SNTConfigurator *configurator = [SNTConfigurator configurator];
SNTDaemonControlController *dc =
@@ -133,13 +136,13 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
if (@available(macOS 13.0, *)) {
SNTEndpointSecurityFileAccessAuthorizer *access_authorizer_client =
[[SNTEndpointSecurityFileAccessAuthorizer alloc]
initWithESAPI:esapi
metrics:metrics
logger:logger
watchItems:watch_items
enricher:enricher
decisionCache:[SNTDecisionCache sharedCache]];
[[SNTEndpointSecurityFileAccessAuthorizer alloc] initWithESAPI:esapi
metrics:metrics
logger:logger
watchItems:watch_items
enricher:enricher
decisionCache:[SNTDecisionCache sharedCache]
ttyWriter:tty_writer];
watch_items->RegisterClient(access_authorizer_client);
access_authorizer_client.fileAccessBlockCallback = ^(SNTFileAccessEvent *event) {

View File

@@ -34,6 +34,7 @@
#import "Source/santad/SNTExecutionController.h"
#import "Source/santad/SNTNotificationQueue.h"
#import "Source/santad/SNTSyncdQueue.h"
#include "Source/santad/TTYWriter.h"
namespace santa::santad {
@@ -56,7 +57,8 @@ class SantadDeps {
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
SNTExecutionController *exec_controller,
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>>
prefix_tree);
prefix_tree,
std::shared_ptr<santa::santad::TTYWriter> tty_writer);
std::shared_ptr<santa::santad::event_providers::AuthResultCache>
AuthResultCache();
@@ -74,6 +76,7 @@ class SantadDeps {
SNTSyncdQueue *SyncdQueue();
SNTExecutionController *ExecController();
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> PrefixTree();
std::shared_ptr<santa::santad::TTYWriter> TTYWriter();
private:
std::shared_ptr<
@@ -93,6 +96,7 @@ class SantadDeps {
SNTSyncdQueue *syncd_queue_;
SNTExecutionController *exec_controller_;
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> prefix_tree_;
std::shared_ptr<santa::santad::TTYWriter> tty_writer_;
};
} // namespace santa::santad

View File

@@ -84,7 +84,7 @@ std::unique_ptr<SantadDeps> SantadDeps::Create(SNTConfigurator *configurator,
exit(EXIT_FAILURE);
}
std::shared_ptr<TTYWriter> tty_writer = TTYWriter::Create();
std::shared_ptr<::TTYWriter> tty_writer = TTYWriter::Create();
if (!tty_writer) {
LOGW(@"Unable to initialize TTY writer");
}
@@ -152,10 +152,10 @@ std::unique_ptr<SantadDeps> SantadDeps::Create(SNTConfigurator *configurator,
exit(EXIT_FAILURE);
}
return std::make_unique<SantadDeps>(esapi, std::move(logger), std::move(metrics),
std::move(watch_items), std::move(auth_result_cache),
control_connection, compiler_controller, notifier_queue,
syncd_queue, exec_controller, prefix_tree);
return std::make_unique<SantadDeps>(
esapi, std::move(logger), std::move(metrics), std::move(watch_items),
std::move(auth_result_cache), control_connection, compiler_controller, notifier_queue,
syncd_queue, exec_controller, prefix_tree, std::move(tty_writer));
}
SantadDeps::SantadDeps(
@@ -164,7 +164,8 @@ SantadDeps::SantadDeps(
std::shared_ptr<santa::santad::event_providers::AuthResultCache> auth_result_cache,
MOLXPCConnection *control_connection, SNTCompilerController *compiler_controller,
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
SNTExecutionController *exec_controller, std::shared_ptr<::PrefixTree<Unit>> prefix_tree)
SNTExecutionController *exec_controller, std::shared_ptr<::PrefixTree<Unit>> prefix_tree,
std::shared_ptr<::TTYWriter> tty_writer)
: esapi_(std::move(esapi)),
logger_(std::move(logger)),
metrics_(std::move(metrics)),
@@ -176,7 +177,8 @@ SantadDeps::SantadDeps(
notifier_queue_(notifier_queue),
syncd_queue_(syncd_queue),
exec_controller_(exec_controller),
prefix_tree_(prefix_tree) {}
prefix_tree_(prefix_tree),
tty_writer_(std::move(tty_writer)) {}
std::shared_ptr<::AuthResultCache> SantadDeps::AuthResultCache() {
return auth_result_cache_;
@@ -225,4 +227,8 @@ std::shared_ptr<PrefixTree<Unit>> SantadDeps::PrefixTree() {
return prefix_tree_;
}
std::shared_ptr<::TTYWriter> SantadDeps::TTYWriter() {
return tty_writer_;
}
} // namespace santa::santad

View File

@@ -153,7 +153,7 @@ int main(int argc, char *argv[]) {
SantadMain(deps->ESAPI(), deps->Logger(), deps->Metrics(), deps->WatchItems(), deps->Enricher(),
deps->AuthResultCache(), deps->ControlConnection(), deps->CompilerController(),
deps->NotifierQueue(), deps->SyncdQueue(), deps->ExecController(),
deps->PrefixTree());
deps->PrefixTree(), deps->TTYWriter());
}
return 0;

Binary file not shown.

View File

@@ -69,6 +69,30 @@
}];
}
if (self.syncState.enableBundles) {
[rop setEnableBundles:[self.syncState.enableBundles boolValue]
reply:^{
}];
}
if (self.syncState.enableTransitiveRules) {
[rop setEnableTransitiveRules:[self.syncState.enableTransitiveRules boolValue]
reply:^{
}];
}
if (self.syncState.enableAllEventUpload) {
[rop setEnableAllEventUpload:[self.syncState.enableAllEventUpload boolValue]
reply:^{
}];
}
if (self.syncState.disableUnknownEventUpload) {
[rop setDisableUnknownEventUpload:[self.syncState.disableUnknownEventUpload boolValue]
reply:^{
}];
}
// Update last sync success
[rop setFullSyncLastSuccess:[NSDate date]
reply:^{

View File

@@ -24,6 +24,15 @@
#import "Source/santasyncservice/SNTSyncLogging.h"
#import "Source/santasyncservice/SNTSyncState.h"
// Return the given value or nil if not of the expected given class
static id EnsureType(id val, Class c) {
if ([val isKindOfClass:c]) {
return val;
} else {
return nil;
}
}
@implementation SNTSyncPreflight
- (NSURL *)stageURL {
@@ -82,44 +91,31 @@
if (!resp) return NO;
NSNumber *enableBundles = resp[kEnableBundles];
if (!enableBundles) enableBundles = resp[kEnableBundlesDeprecated];
[rop setEnableBundles:[enableBundles boolValue]
reply:^{
}];
self.syncState.enableBundles = EnsureType(resp[kEnableBundles], [NSNumber class])
?: EnsureType(resp[kEnableBundlesDeprecated], [NSNumber class]);
self.syncState.enableTransitiveRules = EnsureType(resp[kEnableTransitiveRules], [NSNumber class])
?: EnsureType(resp[kEnableTransitiveRulesDeprecated], [NSNumber class])
?: EnsureType(resp[kEnableTransitiveRulesSuperDeprecated], [NSNumber class]);
self.syncState.enableAllEventUpload = EnsureType(resp[kEnableAllEventUpload], [NSNumber class]);
self.syncState.disableUnknownEventUpload =
EnsureType(resp[kDisableUnknownEventUpload], [NSNumber class]);
NSNumber *enableTransitiveRules = resp[kEnableTransitiveRules];
if (!enableTransitiveRules) enableTransitiveRules = resp[kEnableTransitiveRulesDeprecated];
if (!enableTransitiveRules) enableTransitiveRules = resp[kEnableTransitiveRulesSuperDeprecated];
BOOL enabled = [enableTransitiveRules boolValue];
[rop setEnableTransitiveRules:enabled
reply:^{
}];
NSNumber *enableAllEventUpload = resp[kEnableAllEventUpload];
[rop setEnableAllEventUpload:[enableAllEventUpload boolValue]
reply:^{
}];
NSNumber *disableUnknownEventUpload = resp[kDisableUnknownEventUpload];
[rop setDisableUnknownEventUpload:[disableUnknownEventUpload boolValue]
reply:^{
}];
self.syncState.eventBatchSize = [resp[kBatchSize] unsignedIntegerValue] ?: kDefaultEventBatchSize;
self.syncState.eventBatchSize =
[EnsureType(resp[kBatchSize], [NSNumber class]) unsignedIntegerValue] ?: kDefaultEventBatchSize;
// Don't let these go too low
NSUInteger FCMIntervalValue = [resp[kFCMFullSyncInterval] unsignedIntegerValue];
self.syncState.pushNotificationsFullSyncInterval = (FCMIntervalValue < kDefaultFullSyncInterval)
? kDefaultPushNotificationsFullSyncInterval
: FCMIntervalValue;
FCMIntervalValue = [resp[kFCMGlobalRuleSyncDeadline] unsignedIntegerValue];
NSUInteger value =
[EnsureType(resp[kFCMFullSyncInterval], [NSNumber class]) unsignedIntegerValue];
self.syncState.pushNotificationsFullSyncInterval =
(value < kDefaultFullSyncInterval) ? kDefaultPushNotificationsFullSyncInterval : value;
value = [EnsureType(resp[kFCMGlobalRuleSyncDeadline], [NSNumber class]) unsignedIntegerValue];
self.syncState.pushNotificationsGlobalRuleSyncDeadline =
(FCMIntervalValue < 60) ? kDefaultPushNotificationsGlobalRuleSyncDeadline : FCMIntervalValue;
(value < 60) ? kDefaultPushNotificationsGlobalRuleSyncDeadline : value;
// Check if our sync interval has changed
NSUInteger intervalValue = [resp[kFullSyncInterval] unsignedIntegerValue];
self.syncState.fullSyncInterval = (intervalValue < 60) ? kDefaultFullSyncInterval : intervalValue;
value = [EnsureType(resp[kFullSyncInterval], [NSNumber class]) unsignedIntegerValue];
self.syncState.fullSyncInterval = (value < 60) ? kDefaultFullSyncInterval : value;
if ([resp[kClientMode] isEqual:kClientModeMonitor]) {
self.syncState.clientMode = SNTClientModeMonitor;
@@ -127,27 +123,18 @@
self.syncState.clientMode = SNTClientModeLockdown;
}
if ([resp[kAllowedPathRegex] isKindOfClass:[NSString class]]) {
self.syncState.allowlistRegex = resp[kAllowedPathRegex];
} else if ([resp[kAllowedPathRegexDeprecated] isKindOfClass:[NSString class]]) {
self.syncState.allowlistRegex = resp[kAllowedPathRegexDeprecated];
}
self.syncState.allowlistRegex =
EnsureType(resp[kAllowedPathRegex], [NSString class])
?: EnsureType(resp[kAllowedPathRegexDeprecated], [NSString class]);
if ([resp[kBlockedPathRegex] isKindOfClass:[NSString class]]) {
self.syncState.blocklistRegex = resp[kBlockedPathRegex];
} else if ([resp[kBlockedPathRegexDeprecated] isKindOfClass:[NSString class]]) {
self.syncState.blocklistRegex = resp[kBlockedPathRegexDeprecated];
}
self.syncState.blocklistRegex =
EnsureType(resp[kBlockedPathRegex], [NSString class])
?: EnsureType(resp[kBlockedPathRegexDeprecated], [NSString class]);
if ([resp[kBlockUSBMount] isKindOfClass:[NSNumber class]]) {
self.syncState.blockUSBMount = resp[kBlockUSBMount];
}
self.syncState.blockUSBMount = EnsureType(resp[kBlockUSBMount], [NSNumber class]);
self.syncState.remountUSBMode = EnsureType(resp[kRemountUSBMode], [NSArray class]);
if ([resp[kRemountUSBMode] isKindOfClass:[NSArray class]]) {
self.syncState.remountUSBMode = resp[kRemountUSBMode];
}
if ([resp[kCleanSync] boolValue]) {
if ([EnsureType(resp[kCleanSync], [NSNumber class]) boolValue]) {
SLOGD(@"Clean sync requested by server");
self.syncState.cleanSync = YES;
}

View File

@@ -17,6 +17,7 @@
#import <MOLXPCConnection/MOLXPCConnection.h>
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTSyncConstants.h"
#import "Source/common/SNTXPCControlInterface.h"
@@ -94,11 +95,40 @@
[req setValue:contentEncodingHeader forHTTPHeaderField:@"Content-Encoding"];
}
[self addExtraRequestHeaders:req];
[req setHTTPBody:requestBody];
return req;
}
- (void)addExtraRequestHeaders:(NSMutableURLRequest *)req {
NSDictionary *extra = [[SNTConfigurator configurator] syncExtraHeaders];
[extra enumerateKeysAndObjectsWithOptions:0
usingBlock:^(id key, id object, BOOL *stop) {
if (![key isKindOfClass:[NSString class]] ||
![object isKindOfClass:[NSString class]])
return;
NSString *k = (NSString *)key;
NSString *v = (NSString *)object;
// This is likely unnecessary as the docs for NSURLSession list
// most of these as being ignored but explicitly setting them
// here is an extra layer of protection.
if ([k isEqualToString:@"Content-Encoding"] ||
[k isEqualToString:@"Content-Length"] ||
[k isEqualToString:@"Content-Type"] ||
[k isEqualToString:@"Connection"] ||
[k isEqualToString:@"Host"] ||
[k isEqualToString:@"Proxy-Authenticate"] ||
[k isEqualToString:@"Proxy-Authorization"] ||
[k isEqualToString:@"WWW-Authenticate"])
return;
[req setValue:v forHTTPHeaderField:k];
}];
}
// Returns nil when there is a server connection issue. For other errors, such as
// an empty response or an unparseable response, an empty dictionary is returned.
- (NSDictionary *)performRequest:(NSURLRequest *)request timeout:(NSTimeInterval)timeout {
@@ -106,13 +136,16 @@
NSError *error;
NSData *data;
for (int attempt = 1; attempt < 6; ++attempt) {
if (attempt > 1) {
struct timespec ts = {.tv_sec = (attempt * 2)};
int maxAttempts = 5;
for (int attempt = 1; attempt <= maxAttempts; ++attempt) {
if (attempt >= 2) {
// Exponentially back off with larger and larger delays.
int exponentialBackoffMultiplier = 2; // E.g. 2^2 = 4, 2^3 = 8, 2^4 = 16...
struct timespec ts = {.tv_sec = pow(exponentialBackoffMultiplier, attempt)};
nanosleep(&ts, NULL);
}
SLOGD(@"Performing request, attempt %d", attempt);
SLOGD(@"Performing request, attempt %d (of %d maximum)...", attempt, maxAttempts);
data = [self performRequest:request timeout:timeout response:&response error:&error];
if (response.statusCode == 200) break;

View File

@@ -61,6 +61,10 @@
@property SNTClientMode clientMode;
@property NSString *allowlistRegex;
@property NSString *blocklistRegex;
@property NSNumber *enableBundles;
@property NSNumber *enableTransitiveRules;
@property NSNumber *enableAllEventUpload;
@property NSNumber *disableUnknownEventUpload;
@property NSNumber *blockUSBMount;
// Array of mount args for the forced remounting feature.
@property NSArray *remountUSBMode;

View File

@@ -0,0 +1,5 @@
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -356,12 +356,13 @@ downloading if the rules need to be downloaded in multiple batches.
| Key | Required | Type | Meaning | Example Value |
|---|---|---|---|---|
| identifier | YES | string | The attribute of the binary the rule should match on e.g. the team ID of a binary or sha256 hash value | "ff2a7daa4c25cbd5b057e4471c6a22aba7d154dadfb5cce139c37cf795f41c9c" |
| policy | YES | string | identifies the action to perform in response to the rule matching must be one of the examples. | "ALLOWLIST","ALLOWLIST_COMPILER", "BLOCKLIST", "REMOVE", "SILENT_BLOCKLIST" |
| rule_type | YES | string | identifies the type of rule must be one of he examples | "BINARY", "CERTIFICATE", "TEAMID" |
| custom_msg | NO | string | A custom message to display when the rule matches | "Hello" |
| creation_time | NO | float64 | time the rule was created | 1573543803.349378 |
| file_bundle_binary_count | NO | integer | The number of binaries in a bundle | 13 |
| file_bundle_hash | NO | string | The SHA256 of all binaries in a bundle. | "7466e3687f540bcb7792c6d14d5a186667dbe18a85021857b42effe9f0370805" |
| policy | YES | string | Identifies the action to perform in response to the rule matching (must be one of the examples) | "ALLOWLIST","ALLOWLIST_COMPILER", "BLOCKLIST", "REMOVE", "SILENT_BLOCKLIST" |
| rule\_type | YES | string | Identifies the type of rule (must be one of the examples) | "BINARY", "CERTIFICATE", "SIGNINGID", "TEAMID" |
| custom\_msg | NO | string | A custom message to display when the rule matches | "Hello" |
| custom\_url | NO | string | A custom URL to use for the open button when the rule matches | http://lmgtfy.app/?q=dont+download+malware |
| creation\_time | NO | float64 | Time the rule was created | 1573543803.349378 |
| file\_bundle\_binary\_count | NO | integer | The number of binaries in a bundle | 13 |
| file\_bundle\_hash | NO | string | The SHA256 of all binaries in a bundle | "7466e3687f540bcb7792c6d14d5a186667dbe18a85021857b42effe9f0370805" |
##### Example `ruledownload` Response Payload