mirror of
https://github.com/google/santa.git
synced 2026-01-15 01:08:12 -05:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d82e64aa5f | ||
|
|
a9c1c730be | ||
|
|
6c4362d8bb | ||
|
|
c1189493e8 | ||
|
|
aaa0d40841 | ||
|
|
a424c4afca | ||
|
|
2847397b66 | ||
|
|
ad8b4b6646 | ||
|
|
39ee9e7d48 | ||
|
|
3cccacc3fb | ||
|
|
6ed5bcd808 | ||
|
|
bcac65a23e | ||
|
|
03fcd0c906 | ||
|
|
d3b71a3ba8 |
9
LICENSE
9
LICENSE
@@ -201,12 +201,3 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
------------------
|
||||
|
||||
Files: Testing/integration/VM/*
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
@@ -238,7 +238,10 @@ objc_library(
|
||||
santa_unit_test(
|
||||
name = "SNTRuleTest",
|
||||
srcs = ["SNTRuleTest.m"],
|
||||
deps = [":SNTRule"],
|
||||
deps = [
|
||||
":SNTCommonEnums",
|
||||
":SNTRule",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
@property NSString *quarantineURL;
|
||||
|
||||
@property NSString *customMsg;
|
||||
@property NSString *customURL;
|
||||
@property BOOL silentBlock;
|
||||
|
||||
@end
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]];
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ santa_unit_test(
|
||||
deps = [
|
||||
":fsspool",
|
||||
":fsspool_log_batch_writer",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
@@ -156,6 +157,12 @@ std::string SpoolDirectory(absl::string_view base_dir) {
|
||||
return absl::StrCat(base_dir, PathSeparator(), kSpoolDirName);
|
||||
}
|
||||
|
||||
bool operator==(struct timespec a, struct timespec b) {
|
||||
return a.tv_sec == b.tv_sec && a.tv_nsec == b.tv_nsec;
|
||||
}
|
||||
|
||||
bool operator!=(struct timespec a, struct timespec b) { return !(a == b); }
|
||||
|
||||
} // namespace
|
||||
|
||||
FsSpoolWriter::FsSpoolWriter(absl::string_view base_dir, size_t max_spool_size)
|
||||
@@ -197,6 +204,25 @@ std::string FsSpoolWriter::UniqueFilename() {
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::StatusOr<size_t> FsSpoolWriter::EstimateSpoolDirSize() {
|
||||
struct stat stats;
|
||||
if (stat(spool_dir_.c_str(), &stats) < 0) {
|
||||
return absl::ErrnoToStatus(errno, "failed to stat spool directory");
|
||||
}
|
||||
|
||||
if (stats.st_mtimespec != spool_dir_last_mtime_) {
|
||||
// Store the updated mtime
|
||||
spool_dir_last_mtime_ = stats.st_mtimespec;
|
||||
|
||||
// Recompute the current estimated size
|
||||
return EstimateDirSize(spool_dir_);
|
||||
} else {
|
||||
// If the spool's last modification time hasn't changed then
|
||||
// re-use the current estimate.
|
||||
return spool_size_estimate_;
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status FsSpoolWriter::WriteMessage(absl::string_view msg) {
|
||||
if (absl::Status status = BuildDirectoryStructureIfNeeded(); !status.ok()) {
|
||||
return status; // << "can't create directory structure for writer";
|
||||
@@ -209,7 +235,7 @@ absl::Status FsSpoolWriter::WriteMessage(absl::string_view msg) {
|
||||
// Recompute the spool size if we think we are
|
||||
// over the limit.
|
||||
if (spool_size_estimate_ > max_spool_size_) {
|
||||
absl::StatusOr<size_t> estimate = EstimateDirSize(spool_dir_);
|
||||
absl::StatusOr<size_t> estimate = EstimateSpoolDirSize();
|
||||
if (!estimate.ok()) {
|
||||
return estimate.status(); // failed to recompute spool size
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
|
||||
// Forward declarations
|
||||
namespace fsspool {
|
||||
class FsSpoolWriterPeer;
|
||||
}
|
||||
|
||||
namespace fsspool {
|
||||
|
||||
// Enqueues messages into the spool. Multiple concurrent writers can
|
||||
@@ -42,10 +47,13 @@ class FsSpoolWriter {
|
||||
// returns the UNAVAILABLE canonical code (which is retryable).
|
||||
absl::Status WriteMessage(absl::string_view msg);
|
||||
|
||||
friend class fsspool::FsSpoolWriterPeer;
|
||||
|
||||
private:
|
||||
const std::string base_dir_;
|
||||
const std::string spool_dir_;
|
||||
const std::string tmp_dir_;
|
||||
struct timespec spool_dir_last_mtime_;
|
||||
|
||||
// Approximate maximum size of the spooling area, in bytes. If a message is
|
||||
// being written to a spooling area which already contains more than
|
||||
@@ -81,6 +89,10 @@ class FsSpoolWriter {
|
||||
// Generates a unique filename by combining the random ID of
|
||||
// this writer with a sequence number.
|
||||
std::string UniqueFilename();
|
||||
|
||||
// Estimate the size of the spool directory. However, only recompute a new
|
||||
// estimate if the spool directory has has a change to its modification time.
|
||||
absl::StatusOr<size_t> EstimateSpoolDirSize();
|
||||
};
|
||||
|
||||
// This class is thread-unsafe.
|
||||
|
||||
@@ -33,7 +33,6 @@ FsSpoolLogBatchWriter::~FsSpoolLogBatchWriter() {
|
||||
if (!s.ok()) {
|
||||
os_log(OS_LOG_DEFAULT, "Flush() failed with %s",
|
||||
s.ToString(absl::StatusToStringMode::kWithEverything).c_str());
|
||||
// LOG(WARNING) << "Flush() failed with " << s;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
BIN
Source/santad/testdata/binaryrules/rules.db
vendored
BIN
Source/santad/testdata/binaryrules/rules.db
vendored
Binary file not shown.
@@ -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:^{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
5
Testing/integration/VM/LICENSE
Normal file
5
Testing/integration/VM/LICENSE
Normal 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.
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user