mirror of
https://github.com/google/santa.git
synced 2026-01-15 01:08:12 -05:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5307bd9b7f | ||
|
|
0622e6de71 | ||
|
|
e7c32ae87d | ||
|
|
deaf3a638c | ||
|
|
8a7f1142a8 | ||
|
|
c180205059 | ||
|
|
337df0aa31 | ||
|
|
e2b099aa50 | ||
|
|
fc4e29f34c | ||
|
|
bf3b6bc6e2 | ||
|
|
b810fc81e1 | ||
|
|
3b3aa999c5 | ||
|
|
59428f3be3 | ||
|
|
ae6451a9b2 | ||
|
|
feac080fa7 | ||
|
|
d0f2a0ac4d | ||
|
|
7fc06ea9d8 | ||
|
|
1dfeeac936 | ||
|
|
ac9b5d9399 | ||
|
|
7f3f1c5448 | ||
|
|
46efd6893f | ||
|
|
50232578d6 | ||
|
|
d83be03a20 | ||
|
|
119b29b534 |
2
.github/workflows/sanitizers.yml
vendored
2
.github/workflows/sanitizers.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
DYLIB_PATH="$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/${CLANG_VERSION}/lib/darwin/libclang_rt.${{ matrix.sanitizer }}_osx_dynamic.dylib"
|
||||
|
||||
bazel test --config=${{ matrix.sanitizer }} \
|
||||
--test_strategy=exclusive --test_output=errors \
|
||||
--test_strategy=exclusive --test_output=all \
|
||||
--test_env=DYLD_INSERT_LIBRARIES=${DYLIB_PATH} \
|
||||
--runs_per_test 5 -t- :unit_tests \
|
||||
--define=SANTA_BUILD_TYPE=adhoc
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
# Santa [](https://github.com/google/santa/actions/workflows/ci.yml)
|
||||
# Santa
|
||||
|
||||
[](https://github.com/google/santa/blob/main/LICENSE)
|
||||
[](https://github.com/google/santa/actions/workflows/ci.yml)
|
||||
[](https://github.com/google/santa/releases/latest)
|
||||
[](https://github.com/google/santa/releases/latest)
|
||||
[](https://github.com/google/santa/releases/latest)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/google/santa/main/Source/gui/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
|
||||
|
||||
14
SECURITY.md
14
SECURITY.md
@@ -1,12 +1,14 @@
|
||||
# Reporting a Vulnerability
|
||||
|
||||
If you believe you have found a security vulnerability, we would appreciate private disclosure
|
||||
so that we can work on a fix before disclosure. Any vulnerabilities reported to us will be
|
||||
If you believe you have found a security vulnerability, we would appreciate a private report
|
||||
so that we can work on and release a fix before public disclosure. Any vulnerabilities reported to us will be
|
||||
disclosed publicly either when a new version with fixes is released or 90 days has passed,
|
||||
whichever comes first.
|
||||
|
||||
To report vulnerabilities to us privately, please e-mail `santa-team@google.com`.
|
||||
If you want to encrypt your e-mail, you can use our GPG key `0x92AFE41DAB49BBB6`
|
||||
available on keyserver.ubuntu.com:
|
||||
To report vulnerabilities to us privately, either:
|
||||
|
||||
`gpg --keyserver keyserver.ubuntu.com --recv-key 0x92AFE41DAB49BBB6`
|
||||
1) Report the vulnerability [through GitHub](https://github.com/google/santa/security/advisories/new).
|
||||
|
||||
2) E-mail `santa-team@google.com`. If you want to encrypt your e-mail, you can use our GPG key `0x92AFE41DAB49BBB6` available on keyserver.ubuntu.com:
|
||||
|
||||
`gpg --keyserver keyserver.ubuntu.com --recv-key 0x92AFE41DAB49BBB6`
|
||||
|
||||
@@ -69,6 +69,11 @@ objc_library(
|
||||
hdrs = ["Platform.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "String",
|
||||
hdrs = ["String.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SantaVnodeHash",
|
||||
srcs = ["SantaVnodeHash.mm"],
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
@property SantaVnode vnodeId;
|
||||
@property SNTEventState decision;
|
||||
@property SNTClientMode decisionClientMode;
|
||||
@property NSString *decisionExtra;
|
||||
@property NSString *sha256;
|
||||
|
||||
@@ -36,6 +37,7 @@
|
||||
@property NSString *certCommonName;
|
||||
@property NSArray<MOLCertificate *> *certChain;
|
||||
@property NSString *teamID;
|
||||
@property NSString *signingID;
|
||||
|
||||
@property NSString *quarantineURL;
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ typedef NS_ENUM(NSInteger, SNTRuleType) {
|
||||
SNTRuleTypeBinary = 1,
|
||||
SNTRuleTypeCertificate = 2,
|
||||
SNTRuleTypeTeamID = 3,
|
||||
SNTRuleTypeSigningID = 4,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTRuleState) {
|
||||
@@ -63,32 +64,34 @@ typedef NS_ENUM(NSInteger, SNTClientMode) {
|
||||
SNTClientModeLockdown = 2,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTEventState) {
|
||||
typedef NS_ENUM(uint64_t, SNTEventState) {
|
||||
// Bits 0-15 bits store non-decision types
|
||||
SNTEventStateUnknown = 0,
|
||||
SNTEventStateBundleBinary = 1,
|
||||
|
||||
// Bits 16-23 store deny decision types
|
||||
SNTEventStateBlockUnknown = 1 << 16,
|
||||
SNTEventStateBlockBinary = 1 << 17,
|
||||
SNTEventStateBlockCertificate = 1 << 18,
|
||||
SNTEventStateBlockScope = 1 << 19,
|
||||
SNTEventStateBlockTeamID = 1 << 20,
|
||||
SNTEventStateBlockLongPath = 1 << 21,
|
||||
// Bits 16-39 store deny decision types
|
||||
SNTEventStateBlockUnknown = 1ULL << 16,
|
||||
SNTEventStateBlockBinary = 1ULL << 17,
|
||||
SNTEventStateBlockCertificate = 1ULL << 18,
|
||||
SNTEventStateBlockScope = 1ULL << 19,
|
||||
SNTEventStateBlockTeamID = 1ULL << 20,
|
||||
SNTEventStateBlockLongPath = 1ULL << 21,
|
||||
SNTEventStateBlockSigningID = 1ULL << 22,
|
||||
|
||||
// Bits 24-31 store allow decision types
|
||||
SNTEventStateAllowUnknown = 1 << 24,
|
||||
SNTEventStateAllowBinary = 1 << 25,
|
||||
SNTEventStateAllowCertificate = 1 << 26,
|
||||
SNTEventStateAllowScope = 1 << 27,
|
||||
SNTEventStateAllowCompiler = 1 << 28,
|
||||
SNTEventStateAllowTransitive = 1 << 29,
|
||||
SNTEventStateAllowPendingTransitive = 1 << 30,
|
||||
SNTEventStateAllowTeamID = 1 << 31,
|
||||
// Bits 40-63 store allow decision types
|
||||
SNTEventStateAllowUnknown = 1ULL << 40,
|
||||
SNTEventStateAllowBinary = 1ULL << 41,
|
||||
SNTEventStateAllowCertificate = 1ULL << 42,
|
||||
SNTEventStateAllowScope = 1ULL << 43,
|
||||
SNTEventStateAllowCompiler = 1ULL << 44,
|
||||
SNTEventStateAllowTransitive = 1ULL << 45,
|
||||
SNTEventStateAllowPendingTransitive = 1ULL << 46,
|
||||
SNTEventStateAllowTeamID = 1ULL << 47,
|
||||
SNTEventStateAllowSigningID = 1ULL << 48,
|
||||
|
||||
// Block and Allow masks
|
||||
SNTEventStateBlock = 0xFF << 16,
|
||||
SNTEventStateAllow = 0xFF << 24
|
||||
SNTEventStateBlock = 0xFFFFFFULL << 16,
|
||||
SNTEventStateAllow = 0xFFFFFFULL << 40,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTRuleTableError) {
|
||||
@@ -129,6 +132,12 @@ typedef NS_ENUM(NSInteger, SNTSyncStatusType) {
|
||||
SNTSyncStatusTypeUnknown,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTSyncContentEncoding) {
|
||||
SNTSyncContentEncodingNone,
|
||||
SNTSyncContentEncodingDeflate,
|
||||
SNTSyncContentEncodingGzip,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SNTMetricFormatType) {
|
||||
SNTMetricFormatTypeUnknown,
|
||||
SNTMetricFormatTypeRawJSON,
|
||||
|
||||
@@ -291,6 +291,14 @@
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableSilentMode;
|
||||
|
||||
///
|
||||
/// When silent TTY mode is enabled, Santa will not emit TTY notifications for
|
||||
/// blocked processes.
|
||||
///
|
||||
/// Defaults to NO.
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableSilentTTYMode;
|
||||
|
||||
///
|
||||
/// The text to display when opening Santa.app.
|
||||
/// If unset, the default text will be displayed.
|
||||
@@ -402,6 +410,8 @@
|
||||
///
|
||||
@property(nonatomic) BOOL syncCleanRequired;
|
||||
|
||||
#pragma mark - USB Settings
|
||||
|
||||
///
|
||||
/// USB Mount Blocking. Defaults to false.
|
||||
///
|
||||
@@ -512,6 +522,12 @@
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableBackwardsCompatibleContentEncoding;
|
||||
|
||||
///
|
||||
/// If set, "santactl sync" will use the supplied "Content-Encoding", possible
|
||||
/// settings include "gzip", "deflate", "none". If empty defaults to "deflate".
|
||||
///
|
||||
@property(readonly, nonatomic) SNTSyncContentEncoding syncClientContentEncoding;
|
||||
|
||||
///
|
||||
/// Contains the FCM project name.
|
||||
///
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
@@ -72,6 +73,7 @@ static NSString *const kMachineIDPlistFileKey = @"MachineIDPlist";
|
||||
static NSString *const kMachineIDPlistKeyKey = @"MachineIDKey";
|
||||
|
||||
static NSString *const kEnableSilentModeKey = @"EnableSilentMode";
|
||||
static NSString *const kEnableSilentTTYModeKey = @"EnableSilentTTYMode";
|
||||
static NSString *const kAboutTextKey = @"AboutText";
|
||||
static NSString *const kMoreInfoURLKey = @"MoreInfoURL";
|
||||
static NSString *const kEventDetailURLKey = @"EventDetailURL";
|
||||
@@ -107,8 +109,7 @@ static NSString *const kEnableForkAndExitLogging = @"EnableForkAndExitLogging";
|
||||
static NSString *const kIgnoreOtherEndpointSecurityClients = @"IgnoreOtherEndpointSecurityClients";
|
||||
static NSString *const kEnableDebugLogging = @"EnableDebugLogging";
|
||||
|
||||
static NSString *const kEnableBackwardsCompatibleContentEncoding =
|
||||
@"EnableBackwardsCompatibleContentEncoding";
|
||||
static NSString *const kClientContentEncoding = @"SyncClientContentEncoding";
|
||||
|
||||
static NSString *const kFCMProject = @"FCMProject";
|
||||
static NSString *const kFCMEntity = @"FCMEntity";
|
||||
@@ -128,7 +129,6 @@ static NSString *const kBlockedPathRegexKeyDeprecated = @"BlacklistRegex";
|
||||
static NSString *const kEnableAllEventUploadKey = @"EnableAllEventUpload";
|
||||
static NSString *const kDisableUnknownEventUploadKey = @"DisableUnknownEventUpload";
|
||||
|
||||
// TODO(markowsky): move these to sync server only.
|
||||
static NSString *const kMetricFormat = @"MetricFormat";
|
||||
static NSString *const kMetricURL = @"MetricURL";
|
||||
static NSString *const kMetricExportInterval = @"MetricExportInterval";
|
||||
@@ -181,6 +181,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kEnablePageZeroProtectionKey : number,
|
||||
kEnableBadSignatureProtectionKey : number,
|
||||
kEnableSilentModeKey : number,
|
||||
kEnableSilentTTYModeKey : number,
|
||||
kAboutTextKey : string,
|
||||
kMoreInfoURLKey : string,
|
||||
kEventDetailURLKey : string,
|
||||
@@ -198,6 +199,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kClientAuthCertificatePasswordKey : string,
|
||||
kClientAuthCertificateCNKey : string,
|
||||
kClientAuthCertificateIssuerKey : string,
|
||||
kClientContentEncoding : string,
|
||||
kServerAuthRootsDataKey : data,
|
||||
kServerAuthRootsFileKey : string,
|
||||
kMachineOwnerKey : string,
|
||||
@@ -219,7 +221,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kEnableForkAndExitLogging : number,
|
||||
kIgnoreOtherEndpointSecurityClients : number,
|
||||
kEnableDebugLogging : number,
|
||||
kEnableBackwardsCompatibleContentEncoding : number,
|
||||
kFCMProject : string,
|
||||
kFCMEntity : string,
|
||||
kFCMAPIKey : string,
|
||||
@@ -461,10 +462,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableBackwardsCompatibleContentEncoding {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFcmProject {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -645,6 +642,11 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (BOOL)enableSilentTTYMode {
|
||||
NSNumber *number = self.configState[kEnableSilentTTYModeKey];
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (NSString *)aboutText {
|
||||
return self.configState[kAboutTextKey];
|
||||
}
|
||||
@@ -708,6 +710,20 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return self.configState[kClientAuthCertificateIssuerKey];
|
||||
}
|
||||
|
||||
- (SNTSyncContentEncoding)syncClientContentEncoding {
|
||||
NSString *contentEncoding = [self.configState[kClientContentEncoding] lowercaseString];
|
||||
if ([contentEncoding isEqualToString:@"deflate"]) {
|
||||
return SNTSyncContentEncodingDeflate;
|
||||
} else if ([contentEncoding isEqualToString:@"gzip"]) {
|
||||
return SNTSyncContentEncodingGzip;
|
||||
} else if ([contentEncoding isEqualToString:@"none"]) {
|
||||
return SNTSyncContentEncodingNone;
|
||||
} else {
|
||||
// Ensure we have the same default zlib behavior Santa's always had otherwise.
|
||||
return SNTSyncContentEncodingDeflate;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSData *)syncServerAuthRootsData {
|
||||
return self.configState[kServerAuthRootsDataKey];
|
||||
}
|
||||
@@ -881,11 +897,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [number boolValue] || self.debugFlag;
|
||||
}
|
||||
|
||||
- (BOOL)enableBackwardsCompatibleContentEncoding {
|
||||
NSNumber *number = self.configState[kEnableBackwardsCompatibleContentEncoding];
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (NSString *)fcmProject {
|
||||
return self.configState[kFCMProject];
|
||||
}
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
type = SNTRuleTypeCertificate;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeTeamID]) {
|
||||
type = SNTRuleTypeTeamID;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeSigningID]) {
|
||||
type = SNTRuleTypeSigningID;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -100,6 +100,11 @@
|
||||
///
|
||||
@property NSString *teamID;
|
||||
|
||||
///
|
||||
/// If the executed file was signed, this is the Signing ID if present in the signature information.
|
||||
///
|
||||
@property NSString *signingID;
|
||||
|
||||
///
|
||||
/// The user who executed the binary.
|
||||
///
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
|
||||
ENCODE(self.signingChain, @"signingChain");
|
||||
ENCODE(self.teamID, @"teamID");
|
||||
ENCODE(self.signingID, @"signingID");
|
||||
|
||||
ENCODE(self.executingUser, @"executingUser");
|
||||
ENCODE(self.occurrenceDate, @"occurrenceDate");
|
||||
@@ -95,10 +96,11 @@
|
||||
|
||||
_signingChain = DECODEARRAY(MOLCertificate, @"signingChain");
|
||||
_teamID = DECODE(NSString, @"teamID");
|
||||
_signingID = DECODE(NSString, @"signingID");
|
||||
|
||||
_executingUser = DECODE(NSString, @"executingUser");
|
||||
_occurrenceDate = DECODE(NSDate, @"occurrenceDate");
|
||||
_decision = (SNTEventState)[DECODE(NSNumber, @"decision") intValue];
|
||||
_decision = (SNTEventState)[DECODE(NSNumber, @"decision") unsignedLongLongValue];
|
||||
_pid = DECODE(NSNumber, @"pid");
|
||||
_ppid = DECODE(NSNumber, @"ppid");
|
||||
_parentName = DECODE(NSString, @"parentName");
|
||||
|
||||
@@ -42,6 +42,7 @@ extern NSString *const kCertificateRuleCount;
|
||||
extern NSString *const kCompilerRuleCount;
|
||||
extern NSString *const kTransitiveRuleCount;
|
||||
extern NSString *const kTeamIDRuleCount;
|
||||
extern NSString *const kSigningIDRuleCount;
|
||||
extern NSString *const kFullSyncInterval;
|
||||
extern NSString *const kFCMToken;
|
||||
extern NSString *const kFCMFullSyncInterval;
|
||||
@@ -66,11 +67,13 @@ extern NSString *const kDecisionAllowBinary;
|
||||
extern NSString *const kDecisionAllowCertificate;
|
||||
extern NSString *const kDecisionAllowScope;
|
||||
extern NSString *const kDecisionAllowTeamID;
|
||||
extern NSString *const kDecisionAllowSigningID;
|
||||
extern NSString *const kDecisionBlockUnknown;
|
||||
extern NSString *const kDecisionBlockBinary;
|
||||
extern NSString *const kDecisionBlockCertificate;
|
||||
extern NSString *const kDecisionBlockScope;
|
||||
extern NSString *const kDecisionBlockTeamID;
|
||||
extern NSString *const kDecisionBlockSigningID;
|
||||
extern NSString *const kDecisionUnknown;
|
||||
extern NSString *const kDecisionBundleBinary;
|
||||
extern NSString *const kLoggedInUsers;
|
||||
@@ -95,6 +98,7 @@ extern NSString *const kCertOU;
|
||||
extern NSString *const kCertValidFrom;
|
||||
extern NSString *const kCertValidUntil;
|
||||
extern NSString *const kTeamID;
|
||||
extern NSString *const kSigningID;
|
||||
extern NSString *const kQuarantineDataURL;
|
||||
extern NSString *const kQuarantineRefererURL;
|
||||
extern NSString *const kQuarantineTimestamp;
|
||||
@@ -118,6 +122,7 @@ extern NSString *const kRuleType;
|
||||
extern NSString *const kRuleTypeBinary;
|
||||
extern NSString *const kRuleTypeCertificate;
|
||||
extern NSString *const kRuleTypeTeamID;
|
||||
extern NSString *const kRuleTypeSigningID;
|
||||
extern NSString *const kRuleCustomMsg;
|
||||
extern NSString *const kCursor;
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ NSString *const kCertificateRuleCount = @"certificate_rule_count";
|
||||
NSString *const kCompilerRuleCount = @"compiler_rule_count";
|
||||
NSString *const kTransitiveRuleCount = @"transitive_rule_count";
|
||||
NSString *const kTeamIDRuleCount = @"teamid_rule_count";
|
||||
NSString *const kSigningIDRuleCount = @"signingid_rule_count";
|
||||
NSString *const kFullSyncInterval = @"full_sync_interval";
|
||||
NSString *const kFCMToken = @"fcm_token";
|
||||
NSString *const kFCMFullSyncInterval = @"fcm_full_sync_interval";
|
||||
@@ -67,11 +68,13 @@ NSString *const kDecisionAllowBinary = @"ALLOW_BINARY";
|
||||
NSString *const kDecisionAllowCertificate = @"ALLOW_CERTIFICATE";
|
||||
NSString *const kDecisionAllowScope = @"ALLOW_SCOPE";
|
||||
NSString *const kDecisionAllowTeamID = @"ALLOW_TEAMID";
|
||||
NSString *const kDecisionAllowSigningID = @"ALLOW_SIGNINGID";
|
||||
NSString *const kDecisionBlockUnknown = @"BLOCK_UNKNOWN";
|
||||
NSString *const kDecisionBlockBinary = @"BLOCK_BINARY";
|
||||
NSString *const kDecisionBlockCertificate = @"BLOCK_CERTIFICATE";
|
||||
NSString *const kDecisionBlockScope = @"BLOCK_SCOPE";
|
||||
NSString *const kDecisionBlockTeamID = @"BLOCK_TEAMID";
|
||||
NSString *const kDecisionBlockSigningID = @"BLOCK_SIGNINGID";
|
||||
NSString *const kDecisionUnknown = @"UNKNOWN";
|
||||
NSString *const kDecisionBundleBinary = @"BUNDLE_BINARY";
|
||||
NSString *const kLoggedInUsers = @"logged_in_users";
|
||||
@@ -96,6 +99,7 @@ NSString *const kCertOU = @"ou";
|
||||
NSString *const kCertValidFrom = @"valid_from";
|
||||
NSString *const kCertValidUntil = @"valid_until";
|
||||
NSString *const kTeamID = @"team_id";
|
||||
NSString *const kSigningID = @"signing_id";
|
||||
NSString *const kQuarantineDataURL = @"quarantine_data_url";
|
||||
NSString *const kQuarantineRefererURL = @"quarantine_referer_url";
|
||||
NSString *const kQuarantineTimestamp = @"quarantine_timestamp";
|
||||
@@ -119,6 +123,7 @@ NSString *const kRuleType = @"rule_type";
|
||||
NSString *const kRuleTypeBinary = @"BINARY";
|
||||
NSString *const kRuleTypeCertificate = @"CERTIFICATE";
|
||||
NSString *const kRuleTypeTeamID = @"TEAMID";
|
||||
NSString *const kRuleTypeSigningID = @"SIGNINGID";
|
||||
NSString *const kRuleCustomMsg = @"custom_msg";
|
||||
NSString *const kCursor = @"cursor";
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
- (void)databaseRuleForBinarySHA256:(NSString *)binarySHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
reply:(void (^)(SNTRule *))reply;
|
||||
|
||||
///
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
/// Database ops
|
||||
///
|
||||
- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive, int64_t teamID))reply;
|
||||
int64_t transitive, int64_t teamID, int64_t signingID))reply;
|
||||
- (void)databaseEventCount:(void (^)(int64_t count))reply;
|
||||
- (void)staticRuleCount:(void (^)(int64_t count))reply;
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
fileSHA256:(NSString *)fileSHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
reply:(void (^)(SNTEventState))reply;
|
||||
|
||||
///
|
||||
|
||||
35
Source/common/String.h
Normal file
35
Source/common/String.h
Normal file
@@ -0,0 +1,35 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__COMMON__STRING_H
|
||||
#define SANTA__COMMON__STRING_H
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace santa::common {
|
||||
|
||||
static inline std::string_view NSStringToUTF8StringView(NSString *str) {
|
||||
return std::string_view(str.UTF8String, [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
|
||||
}
|
||||
|
||||
static inline std::string NSStringToUTF8String(NSString *str) {
|
||||
return std::string(str.UTF8String, [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
|
||||
}
|
||||
|
||||
} // namespace santa::common
|
||||
|
||||
#endif
|
||||
@@ -262,6 +262,7 @@ message Execution {
|
||||
REASON_TRANSITIVE = 8;
|
||||
REASON_LONG_PATH = 9;
|
||||
REASON_NOT_RUNNING = 10;
|
||||
REASON_SIGNING_ID = 11;
|
||||
}
|
||||
optional Reason reason = 10;
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ struct SNTDeviceMessageWindowView: View {
|
||||
Text("Device Name").bold()
|
||||
Text("Device BSD Path").bold()
|
||||
|
||||
if event!.remountArgs.count > 0 {
|
||||
if event!.remountArgs?.count ?? 0 > 0 {
|
||||
Text("Remount Mode").bold()
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ struct SNTDeviceMessageWindowView: View {
|
||||
Text(event!.mntonname)
|
||||
Text(event!.mntfromname)
|
||||
|
||||
if event!.remountArgs.count > 0 {
|
||||
if event!.remountArgs?.count ?? 0 > 0 {
|
||||
Text(event!.readableRemountArgs())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,14 +37,14 @@ objc_library(
|
||||
objc_library(
|
||||
name = "santactl_lib",
|
||||
srcs = [
|
||||
"main.m",
|
||||
"Commands/SNTCommandFileInfo.m",
|
||||
"Commands/SNTCommandRule.m",
|
||||
"Commands/SNTCommandStatus.m",
|
||||
"Commands/SNTCommandVersion.m",
|
||||
"Commands/SNTCommandMetrics.h",
|
||||
"Commands/SNTCommandMetrics.m",
|
||||
"Commands/SNTCommandRule.m",
|
||||
"Commands/SNTCommandStatus.m",
|
||||
"Commands/SNTCommandSync.m",
|
||||
"Commands/SNTCommandVersion.m",
|
||||
"main.m",
|
||||
] + select({
|
||||
"//:opt_build": [],
|
||||
"//conditions:default": [
|
||||
|
||||
@@ -42,6 +42,7 @@ static NSString *const kRule = @"Rule";
|
||||
static NSString *const kSigningChain = @"Signing Chain";
|
||||
static NSString *const kUniversalSigningChain = @"Universal Signing Chain";
|
||||
static NSString *const kTeamID = @"Team ID";
|
||||
static NSString *const kSigningID = @"Signing ID";
|
||||
|
||||
// signing chain keys
|
||||
static NSString *const kCommonName = @"Common Name";
|
||||
@@ -111,6 +112,7 @@ typedef id (^SNTAttributeBlock)(SNTCommandFileInfo *, SNTFileInfo *);
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock downloadTimestamp;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock downloadAgent;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock teamID;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock signingID;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock type;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock pageZero;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock codeSigned;
|
||||
@@ -184,8 +186,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
+ (NSArray<NSString *> *)fileInfoKeys {
|
||||
return @[
|
||||
kPath, kSHA256, kSHA1, kBundleName, kBundleVersion, kBundleVersionStr, kDownloadReferrerURL,
|
||||
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kType, kPageZero, kCodeSigned, kRule,
|
||||
kSigningChain, kUniversalSigningChain
|
||||
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kSigningID, kType, kPageZero,
|
||||
kCodeSigned, kRule, kSigningChain, kUniversalSigningChain
|
||||
];
|
||||
}
|
||||
|
||||
@@ -218,6 +220,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
kSigningChain : self.signingChain,
|
||||
kUniversalSigningChain : self.universalSigningChain,
|
||||
kTeamID : self.teamID,
|
||||
kSigningID : self.signingID,
|
||||
};
|
||||
|
||||
_printQueue = dispatch_queue_create("com.google.santactl.print_queue", DISPATCH_QUEUE_SERIAL);
|
||||
@@ -357,15 +360,34 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
NSError *err;
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:&err];
|
||||
[[cmd.daemonConn remoteObjectProxy]
|
||||
decisionForFilePath:fileInfo.path
|
||||
fileSHA256:fileInfo.SHA256
|
||||
certificateSHA256:err ? nil : csc.leafCertificate.SHA256
|
||||
teamID:[csc.signingInformation valueForKey:@"teamid"]
|
||||
reply:^(SNTEventState s) {
|
||||
state = s;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
|
||||
NSString *teamID =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier];
|
||||
NSString *identifier =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
|
||||
|
||||
NSString *signingID;
|
||||
if (identifier) {
|
||||
if (teamID) {
|
||||
signingID = [NSString stringWithFormat:@"%@:%@", teamID, identifier];
|
||||
} else {
|
||||
id platformID =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoPlatformIdentifier];
|
||||
if ([platformID isKindOfClass:[NSNumber class]] && [platformID intValue] != 0) {
|
||||
signingID = [NSString stringWithFormat:@"platform:%@", identifier];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[[cmd.daemonConn remoteObjectProxy] decisionForFilePath:fileInfo.path
|
||||
fileSHA256:fileInfo.SHA256
|
||||
certificateSHA256:err ? nil : csc.leafCertificate.SHA256
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
reply:^(SNTEventState s) {
|
||||
state = s;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
|
||||
cmd.daemonUnavailable = YES;
|
||||
return kCommunicationErrorMsg;
|
||||
@@ -381,6 +403,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
case SNTEventStateBlockCertificate: [output appendString:@" (Certificate)"]; break;
|
||||
case SNTEventStateAllowTeamID:
|
||||
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
|
||||
case SNTEventStateAllowSigningID:
|
||||
case SNTEventStateBlockSigningID: [output appendString:@" (SigningID)"]; break;
|
||||
case SNTEventStateAllowScope:
|
||||
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
|
||||
case SNTEventStateAllowCompiler: [output appendString:@" (Compiler)"]; break;
|
||||
@@ -473,6 +497,13 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
};
|
||||
}
|
||||
|
||||
- (SNTAttributeBlock)signingID {
|
||||
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
|
||||
return [csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
// Entry point for the command.
|
||||
|
||||
@@ -60,16 +60,28 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
@" Will add the hash of the file currently at that path.\n"
|
||||
@" Does not work with --check. Use the fileinfo verb to check.\n"
|
||||
@" the rule state of a file.\n"
|
||||
@" --identifier {sha256|teamID}: identifier to add/remove/check\n"
|
||||
@" --identifier {sha256|teamID|signingID}: identifier to add/remove/check\n"
|
||||
@" --sha256 {sha256}: hash to add/remove/check [deprecated]\n"
|
||||
@"\n"
|
||||
@" Optionally:\n"
|
||||
@" --teamid: add or check a team ID rule instead of binary\n"
|
||||
@" --signingid: add or check a signing ID rule instead of binary (see notes)\n"
|
||||
@" --certificate: add or check a certificate sha256 rule instead of binary\n"
|
||||
#ifdef DEBUG
|
||||
@" --force: allow manual changes even when SyncBaseUrl is set\n"
|
||||
#endif
|
||||
@" --message {message}: custom message\n");
|
||||
@" --message {message}: custom message\n"
|
||||
@"\n"
|
||||
@" Notes:\n"
|
||||
@" The format of `identifier` when adding/checking a `signingid` rule is:\n"
|
||||
@"\n"
|
||||
@" `TeamID:SigningID`\n"
|
||||
@"\n"
|
||||
@" Because signing IDs are controlled by the binary author, this ensures\n"
|
||||
@" that the signing ID is properly scoped to a developer. For the special\n"
|
||||
@" case of platform binaries, `TeamID` should be replaced with the string\n"
|
||||
@" \"platform\" (e.g. `platform:SigningID`). This allows for rules\n"
|
||||
@" targeting Apple-signed binaries that do not have a team ID.\n");
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
@@ -116,6 +128,8 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
newRule.type = SNTRuleTypeCertificate;
|
||||
} else if ([arg caseInsensitiveCompare:@"--teamid"] == NSOrderedSame) {
|
||||
newRule.type = SNTRuleTypeTeamID;
|
||||
} else if ([arg caseInsensitiveCompare:@"--signingid"] == NSOrderedSame) {
|
||||
newRule.type = SNTRuleTypeSigningID;
|
||||
} else if ([arg caseInsensitiveCompare:@"--path"] == NSOrderedSame) {
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"--path requires an argument"];
|
||||
@@ -145,6 +159,22 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
}
|
||||
}
|
||||
|
||||
if (path) {
|
||||
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithPath:path];
|
||||
if (!fi.path) {
|
||||
[self printErrorUsageAndExit:@"Provided path was not a plain file"];
|
||||
}
|
||||
|
||||
if (newRule.type == SNTRuleTypeBinary) {
|
||||
newRule.identifier = fi.SHA256;
|
||||
} else if (newRule.type == SNTRuleTypeCertificate) {
|
||||
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
|
||||
newRule.identifier = cs.leafCertificate.SHA256;
|
||||
} else if (newRule.type == SNTRuleTypeTeamID || newRule.type == SNTRuleTypeSigningID) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
if (newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate) {
|
||||
NSCharacterSet *nonHex =
|
||||
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
|
||||
@@ -159,21 +189,6 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
return [self printStateOfRule:newRule daemonConnection:self.daemonConn];
|
||||
}
|
||||
|
||||
if (path) {
|
||||
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithPath:path];
|
||||
if (!fi.path) {
|
||||
[self printErrorUsageAndExit:@"Provided path was not a plain file"];
|
||||
}
|
||||
|
||||
if (newRule.type == SNTRuleTypeBinary) {
|
||||
newRule.identifier = fi.SHA256;
|
||||
} else if (newRule.type == SNTRuleTypeCertificate) {
|
||||
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
|
||||
newRule.identifier = cs.leafCertificate.SHA256;
|
||||
} else if (newRule.type == SNTRuleTypeTeamID) {
|
||||
}
|
||||
}
|
||||
|
||||
if (newRule.state == SNTRuleStateUnknown) {
|
||||
[self printErrorUsageAndExit:@"No state specified"];
|
||||
} else if (!newRule.identifier) {
|
||||
@@ -220,11 +235,13 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
NSString *fileSHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil;
|
||||
NSString *certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil;
|
||||
NSString *teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil;
|
||||
NSString *signingID = (rule.type == SNTRuleTypeSigningID) ? rule.identifier : nil;
|
||||
__block NSMutableString *output;
|
||||
[rop decisionForFilePath:nil
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
reply:^(SNTEventState s) {
|
||||
output =
|
||||
(SNTEventStateAllow & s) ? @"Allowed".mutableCopy : @"Blocked".mutableCopy;
|
||||
@@ -247,6 +264,10 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
break;
|
||||
case SNTEventStateAllowTeamID:
|
||||
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
|
||||
case SNTEventStateAllowSigningID:
|
||||
case SNTEventStateBlockSigningID:
|
||||
[output appendString:@" (SigningID)"];
|
||||
break;
|
||||
default: output = @"None".mutableCopy; break;
|
||||
}
|
||||
if (isatty(STDOUT_FILENO)) {
|
||||
@@ -266,6 +287,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
[rop databaseRuleForBinarySHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
reply:^(SNTRule *r) {
|
||||
if (r.state == SNTRuleStateAllowTransitive) {
|
||||
NSDate *date =
|
||||
|
||||
@@ -81,13 +81,19 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
}];
|
||||
|
||||
// Database counts
|
||||
__block int64_t eventCount = -1, binaryRuleCount = -1, certRuleCount = -1, teamIDRuleCount = -1;
|
||||
__block int64_t compilerRuleCount = -1, transitiveRuleCount = -1;
|
||||
__block int64_t eventCount = -1;
|
||||
__block int64_t binaryRuleCount = -1;
|
||||
__block int64_t certRuleCount = -1;
|
||||
__block int64_t teamIDRuleCount = -1;
|
||||
__block int64_t signingIDRuleCount = -1;
|
||||
__block int64_t compilerRuleCount = -1;
|
||||
__block int64_t transitiveRuleCount = -1;
|
||||
[rop databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive, int64_t teamID) {
|
||||
int64_t transitive, int64_t teamID, int64_t signingID) {
|
||||
binaryRuleCount = binary;
|
||||
certRuleCount = certificate;
|
||||
teamIDRuleCount = teamID;
|
||||
signingIDRuleCount = signingID;
|
||||
compilerRuleCount = compiler;
|
||||
transitiveRuleCount = transitive;
|
||||
}];
|
||||
@@ -193,6 +199,8 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"database" : @{
|
||||
@"binary_rules" : @(binaryRuleCount),
|
||||
@"certificate_rules" : @(certRuleCount),
|
||||
@"teamid_rules" : @(teamIDRuleCount),
|
||||
@"signingid_rules" : @(signingIDRuleCount),
|
||||
@"compiler_rules" : @(compilerRuleCount),
|
||||
@"transitive_rules" : @(transitiveRuleCount),
|
||||
@"events_pending_upload" : @(eventCount),
|
||||
@@ -258,6 +266,7 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
printf(" %-25s | %lld\n", "Binary Rules", binaryRuleCount);
|
||||
printf(" %-25s | %lld\n", "Certificate Rules", certRuleCount);
|
||||
printf(" %-25s | %lld\n", "TeamID Rules", teamIDRuleCount);
|
||||
printf(" %-25s | %lld\n", "SigningID Rules", signingIDRuleCount);
|
||||
printf(" %-25s | %lld\n", "Compiler Rules", compilerRuleCount);
|
||||
printf(" %-25s | %lld\n", "Transitive Rules", transitiveRuleCount);
|
||||
printf(" %-25s | %lld\n", "Events Pending Upload", eventCount);
|
||||
|
||||
@@ -49,6 +49,7 @@ objc_library(
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:String",
|
||||
"//Source/common:Unit",
|
||||
],
|
||||
)
|
||||
@@ -232,6 +233,7 @@ objc_library(
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:String",
|
||||
"@MOLCodesignChecker",
|
||||
],
|
||||
)
|
||||
@@ -382,6 +384,7 @@ objc_library(
|
||||
":EndpointSecurityClient",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
@@ -468,6 +471,7 @@ objc_library(
|
||||
],
|
||||
deps = [
|
||||
":EndpointSecurityMessage",
|
||||
"//Source/common:String",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -482,7 +486,6 @@ objc_library(
|
||||
":EndpointSecuritySerializerUtilities",
|
||||
":SNTDecisionCache",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
],
|
||||
@@ -501,6 +504,7 @@ objc_library(
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:String",
|
||||
"//Source/common:santa_cc_proto_library_wrapper",
|
||||
],
|
||||
)
|
||||
@@ -569,6 +573,7 @@ objc_library(
|
||||
":EndpointSecurityWriterNull",
|
||||
":EndpointSecurityWriterSpool",
|
||||
":EndpointSecurityWriterSyslog",
|
||||
":SNTDecisionCache",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
@@ -694,6 +699,7 @@ objc_library(
|
||||
":Metrics",
|
||||
":SNTCompilerController",
|
||||
":SNTDatabaseController",
|
||||
":SNTDecisionCache",
|
||||
":SNTEventTable",
|
||||
":SNTExecutionController",
|
||||
":SNTNotificationQueue",
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
bail = YES;
|
||||
return;
|
||||
}
|
||||
[db close];
|
||||
[[NSFileManager defaultManager] removeItemAtPath:[db databasePath] error:NULL];
|
||||
[db open];
|
||||
[self closeDeleteReopenDatabase:db];
|
||||
} else if ([db userVersion] > [self currentSupportedVersion]) {
|
||||
[self closeDeleteReopenDatabase:db];
|
||||
}
|
||||
}];
|
||||
|
||||
@@ -58,11 +58,22 @@
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)closeDeleteReopenDatabase:(FMDatabase *)db {
|
||||
[db close];
|
||||
[[NSFileManager defaultManager] removeItemAtPath:[db databasePath] error:NULL];
|
||||
[db open];
|
||||
}
|
||||
|
||||
- (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (uint32_t)currentSupportedVersion {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Called at the end of initialization to ensure the table in the
|
||||
/// database exists and uses the latest schema.
|
||||
- (void)updateTableSchema {
|
||||
|
||||
@@ -18,8 +18,14 @@
|
||||
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
|
||||
static const uint32_t kEventTableCurrentVersion = 3;
|
||||
|
||||
@implementation SNTEventTable
|
||||
|
||||
- (uint32_t)currentSupportedVersion {
|
||||
return kEventTableCurrentVersion;
|
||||
}
|
||||
|
||||
- (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version {
|
||||
int newVersion = 0;
|
||||
|
||||
|
||||
@@ -57,10 +57,16 @@
|
||||
- (NSUInteger)teamIDRuleCount;
|
||||
|
||||
///
|
||||
/// @return Rule for binary or certificate with given SHA-256. The binary rule will be returned
|
||||
/// if it exists. If not, the certificate rule will be returned if it exists.
|
||||
/// @return Number of signing ID rules in the database
|
||||
///
|
||||
- (NSUInteger)signingIDRuleCount;
|
||||
|
||||
///
|
||||
/// @return Rule for binary, signingID, certificate or teamID (in that order).
|
||||
/// The first matching rule found is returned.
|
||||
///
|
||||
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
|
||||
signingID:(NSString *)signingID
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID;
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
|
||||
static const uint32_t kRuleTableCurrentVersion = 4;
|
||||
|
||||
// TODO(nguyenphillip): this should be configurable.
|
||||
// How many rules must be in database before we start trying to remove transitive rules.
|
||||
static const NSUInteger kTransitiveRuleCullingThreshold = 500000;
|
||||
@@ -173,6 +175,10 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
self.criticalSystemBinaries = bins;
|
||||
}
|
||||
|
||||
- (uint32_t)currentSupportedVersion {
|
||||
return kRuleTableCurrentVersion;
|
||||
}
|
||||
|
||||
- (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version {
|
||||
// Lock this database from other processes
|
||||
[[db executeQuery:@"PRAGMA locking_mode = EXCLUSIVE;"] close];
|
||||
@@ -272,6 +278,14 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSUInteger)signingIDRuleCount {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=4"];
|
||||
}];
|
||||
return count;
|
||||
}
|
||||
|
||||
- (SNTRule *)ruleFromResultSet:(FMResultSet *)rs {
|
||||
return [[SNTRule alloc] initWithIdentifier:[rs stringForColumn:@"identifier"]
|
||||
state:[rs intForColumn:@"state"]
|
||||
@@ -281,6 +295,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
}
|
||||
|
||||
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
|
||||
signingID:(NSString *)signingID
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID {
|
||||
__block SNTRule *rule;
|
||||
@@ -288,12 +303,27 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
// Look for a static rule that matches.
|
||||
NSDictionary *staticRules = [[SNTConfigurator configurator] staticRules];
|
||||
if (staticRules.count) {
|
||||
// IMPORTANT: The order static rules are checked here should be the same
|
||||
// order as given by the SQL query for the rules database.
|
||||
rule = staticRules[binarySHA256];
|
||||
if (rule.type == SNTRuleTypeBinary) return rule;
|
||||
if (rule.type == SNTRuleTypeBinary) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[signingID];
|
||||
if (rule.type == SNTRuleTypeSigningID) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[certificateSHA256];
|
||||
if (rule.type == SNTRuleTypeCertificate) return rule;
|
||||
if (rule.type == SNTRuleTypeCertificate) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[teamID];
|
||||
if (rule.type == SNTRuleTypeTeamID) return rule;
|
||||
if (rule.type == SNTRuleTypeTeamID) {
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
|
||||
// Now query the database.
|
||||
@@ -301,7 +331,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
// NOTE: This code is written with the intention that the binary rule is searched for first
|
||||
// as Santa is designed to go with the most-specific rule possible.
|
||||
//
|
||||
// The intended order of precedence is Binaries > Certificates > Team IDs.
|
||||
// The intended order of precedence is Binaries > Signing IDs > Certificates > Team IDs.
|
||||
//
|
||||
// As such the query should have "ORDER BY type DESC" before the LIMIT, to ensure that is the
|
||||
// case. However, in all tested versions of SQLite that ORDER BY clause is unnecessary: the query
|
||||
@@ -316,10 +346,12 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
// There is a test for this in SNTRuleTableTests in case SQLite behavior changes in the future.
|
||||
//
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
FMResultSet *rs =
|
||||
[db executeQuery:@"SELECT * FROM rules WHERE (identifier=? and type=1) OR "
|
||||
@"(identifier=? AND type=2) OR (identifier=? AND type=3) LIMIT 1",
|
||||
binarySHA256, certificateSHA256, teamID];
|
||||
FMResultSet *rs = [db executeQuery:@"SELECT * FROM rules WHERE "
|
||||
@" (identifier=? and type=1) "
|
||||
@"OR (identifier=? AND type=4) "
|
||||
@"OR (identifier=? AND type=2) "
|
||||
@"OR (identifier=? AND type=3) LIMIT 1",
|
||||
binarySHA256, signingID, certificateSHA256, teamID];
|
||||
if ([rs next]) {
|
||||
rule = [self ruleFromResultSet:rs];
|
||||
}
|
||||
|
||||
@@ -43,6 +43,19 @@
|
||||
return r;
|
||||
}
|
||||
|
||||
- (SNTRule *)_exampleSigningIDRuleIsPlatform:(BOOL)isPlatformBinary {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
if (isPlatformBinary) {
|
||||
r.identifier = @"platform:signingID";
|
||||
} else {
|
||||
r.identifier = @"teamID:signingID";
|
||||
}
|
||||
r.state = SNTRuleStateBlock;
|
||||
r.type = SNTRuleTypeSigningID;
|
||||
r.customMsg = @"A teamID rule";
|
||||
return r;
|
||||
}
|
||||
|
||||
- (SNTRule *)_exampleBinaryRule {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
r.identifier = @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670";
|
||||
@@ -127,6 +140,7 @@
|
||||
|
||||
SNTRule *r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
XCTAssertNotNil(r);
|
||||
@@ -136,6 +150,7 @@
|
||||
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"b6ee1c3c5a715c049d14a8457faa6b6701b8507efe908300e238e0768bd759c2"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
XCTAssertNil(r);
|
||||
@@ -148,6 +163,7 @@
|
||||
|
||||
SNTRule *r = [self.sut
|
||||
ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:nil];
|
||||
XCTAssertNotNil(r);
|
||||
@@ -157,6 +173,7 @@
|
||||
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:@"5bdab1288fc16892fef50c658db54f1e2e19cf8f71cc55f77de2b95e051e2562"
|
||||
teamID:nil];
|
||||
XCTAssertNil(r);
|
||||
@@ -167,26 +184,66 @@
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil certificateSHA256:nil teamID:@"teamID"];
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeTeamID);
|
||||
XCTAssertEqual([self.sut teamIDRuleCount], 1);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil certificateSHA256:nil teamID:@"nonexistentTeamID"];
|
||||
r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@"nonexistentTeamID"];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
- (void)testFetchSigningIDRule {
|
||||
[self.sut addRules:@[
|
||||
[self _exampleBinaryRule], [self _exampleSigningIDRuleIsPlatform:YES],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO]
|
||||
]
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
|
||||
XCTAssertEqual([self.sut signingIDRuleCount], 2);
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:@"teamID:signingID"
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:@"platform:signingID"
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"platform:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil signingID:@"nonexistent" certificateSHA256:nil teamID:nil];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
- (void)testFetchRuleOrdering {
|
||||
[self.sut
|
||||
addRules:@[ [self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule] ]
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
[self.sut addRules:@[
|
||||
[self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO]
|
||||
]
|
||||
cleanSlate:NO
|
||||
error:nil];
|
||||
|
||||
// This test verifies that the implicit rule ordering we've been abusing is still working.
|
||||
// See the comment in SNTRuleTable#ruleForBinarySHA256:certificateSHA256:teamID
|
||||
SNTRule *r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:@"teamID:signingID"
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
@@ -196,6 +253,7 @@
|
||||
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:@"teamID:signingID"
|
||||
certificateSHA256:@"unknowncert"
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
@@ -205,12 +263,29 @@
|
||||
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"unknown"
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCertificate, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"teamID:signingID"
|
||||
certificateSHA256:@"unknown"
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID, @"Implicit rule ordering failed (SigningID)");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"unknown"
|
||||
certificateSHA256:@"unknown"
|
||||
teamID:@"teamID"];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"teamID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeTeamID, @"Implicit rule ordering failed (TeamID)");
|
||||
}
|
||||
|
||||
- (void)testBadDatabase {
|
||||
|
||||
@@ -33,6 +33,7 @@ static constexpr WatchItemPathType kWatchItemPolicyDefaultPathType =
|
||||
WatchItemPathType::kLiteral;
|
||||
static constexpr bool kWatchItemPolicyDefaultAllowReadAccess = false;
|
||||
static constexpr bool kWatchItemPolicyDefaultAuditOnly = true;
|
||||
static constexpr bool kWatchItemPolicyDefaultInvertProcessExceptions = false;
|
||||
|
||||
struct WatchItemPolicy {
|
||||
struct Process {
|
||||
@@ -69,19 +70,23 @@ struct WatchItemPolicy {
|
||||
WatchItemPathType pt = kWatchItemPolicyDefaultPathType,
|
||||
bool ara = kWatchItemPolicyDefaultAllowReadAccess,
|
||||
bool ao = kWatchItemPolicyDefaultAuditOnly,
|
||||
bool ipe = kWatchItemPolicyDefaultInvertProcessExceptions,
|
||||
std::vector<Process> procs = {})
|
||||
: name(n),
|
||||
path(p),
|
||||
path_type(pt),
|
||||
allow_read_access(ara),
|
||||
audit_only(ao),
|
||||
invert_process_exceptions(ipe),
|
||||
processes(std::move(procs)) {}
|
||||
|
||||
bool operator==(const WatchItemPolicy &other) const {
|
||||
return name == other.name && path == other.path &&
|
||||
path_type == other.path_type &&
|
||||
allow_read_access == other.allow_read_access &&
|
||||
audit_only == other.audit_only && processes == other.processes;
|
||||
audit_only == other.audit_only &&
|
||||
invert_process_exceptions == other.invert_process_exceptions &&
|
||||
processes == other.processes;
|
||||
}
|
||||
|
||||
bool operator!=(const WatchItemPolicy &other) const {
|
||||
@@ -93,6 +98,7 @@ struct WatchItemPolicy {
|
||||
WatchItemPathType path_type;
|
||||
bool allow_read_access;
|
||||
bool audit_only;
|
||||
bool invert_process_exceptions;
|
||||
std::vector<Process> processes;
|
||||
};
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ extern NSString *const kWatchItemConfigKeyPathsIsPrefix;
|
||||
extern NSString *const kWatchItemConfigKeyOptions;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsAllowReadAccess;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsAuditOnly;
|
||||
extern NSString *const kWatchItemConfigKeyOptionsInvertProcessExceptions;
|
||||
extern NSString *const kWatchItemConfigKeyProcesses;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesBinaryPath;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesCertificateSha256;
|
||||
|
||||
@@ -34,8 +34,12 @@
|
||||
|
||||
#import "Source/common/PrefixTree.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/String.h"
|
||||
#import "Source/common/Unit.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
|
||||
using santa::common::NSStringToUTF8String;
|
||||
using santa::common::NSStringToUTF8StringView;
|
||||
using santa::common::PrefixTree;
|
||||
using santa::common::Unit;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
@@ -49,6 +53,7 @@ NSString *const kWatchItemConfigKeyPathsIsPrefix = @"IsPrefix";
|
||||
NSString *const kWatchItemConfigKeyOptions = @"Options";
|
||||
NSString *const kWatchItemConfigKeyOptionsAllowReadAccess = @"AllowReadAccess";
|
||||
NSString *const kWatchItemConfigKeyOptionsAuditOnly = @"AuditOnly";
|
||||
NSString *const kWatchItemConfigKeyOptionsInvertProcessExceptions = @"InvertProcessExceptions";
|
||||
NSString *const kWatchItemConfigKeyProcesses = @"Processes";
|
||||
NSString *const kWatchItemConfigKeyProcessesBinaryPath = @"BinaryPath";
|
||||
NSString *const kWatchItemConfigKeyProcessesCertificateSha256 = @"CertificateSha256";
|
||||
@@ -255,7 +260,7 @@ std::variant<Unit, PathList> VerifyConfigWatchItemPaths(NSArray<id> *paths, NSEr
|
||||
return Unit{};
|
||||
}
|
||||
|
||||
path_list.push_back({std::string(path_str.UTF8String, path_str.length), path_type});
|
||||
path_list.push_back({NSStringToUTF8String(path_str), path_type});
|
||||
} else if ([path isKindOfClass:[NSString class]]) {
|
||||
if (!LenRangeValidator(1, PATH_MAX)(path, err)) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Invalid path length: %@",
|
||||
@@ -264,8 +269,8 @@ std::variant<Unit, PathList> VerifyConfigWatchItemPaths(NSArray<id> *paths, NSEr
|
||||
return Unit{};
|
||||
}
|
||||
|
||||
path_list.push_back({std::string(((NSString *)path).UTF8String, ((NSString *)path).length),
|
||||
kWatchItemPolicyDefaultPathType});
|
||||
path_list.push_back(
|
||||
{NSStringToUTF8String(((NSString *)path)), kWatchItemPolicyDefaultPathType});
|
||||
} else {
|
||||
PopulateError(
|
||||
err, [NSString stringWithFormat:
|
||||
@@ -340,12 +345,11 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
|
||||
}
|
||||
|
||||
proc_list.push_back(WatchItemPolicy::Process(
|
||||
std::string([(process[kWatchItemConfigKeyProcessesBinaryPath] ?: @"") UTF8String]),
|
||||
std::string([(process[kWatchItemConfigKeyProcessesSigningID] ?: @"") UTF8String]),
|
||||
std::string([(process[kWatchItemConfigKeyProcessesTeamID] ?: @"") UTF8String]),
|
||||
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesBinaryPath] ?: @""),
|
||||
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesSigningID] ?: @""),
|
||||
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesTeamID] ?: @""),
|
||||
HexStringToBytes(process[kWatchItemConfigKeyProcessesCDHash]),
|
||||
std::string(
|
||||
[(process[kWatchItemConfigKeyProcessesCertificateSha256] ?: @"") UTF8String]),
|
||||
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesCertificateSha256] ?: @""),
|
||||
process[kWatchItemConfigKeyProcessesPlatformBinary]
|
||||
? std::make_optional(
|
||||
(bool)[process[kWatchItemConfigKeyProcessesPlatformBinary] boolValue])
|
||||
@@ -373,6 +377,8 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
|
||||
/// <false/>
|
||||
/// <key>AuditOnly</key>
|
||||
/// <false/>
|
||||
/// <key>InvertProcessExceptions</key>
|
||||
/// <false/>
|
||||
/// </dict>
|
||||
/// <key>Processes</key>
|
||||
/// <array>
|
||||
@@ -407,6 +413,11 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsAuditOnly, [NSNumber class], err)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VerifyConfigKey(options, kWatchItemConfigKeyOptionsInvertProcessExceptions,
|
||||
[NSNumber class], err)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool allow_read_access = options[kWatchItemConfigKeyOptionsAllowReadAccess]
|
||||
@@ -415,6 +426,10 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
bool audit_only = options[kWatchItemConfigKeyOptionsAuditOnly]
|
||||
? [options[kWatchItemConfigKeyOptionsAuditOnly] boolValue]
|
||||
: kWatchItemPolicyDefaultAuditOnly;
|
||||
bool invert_process_exceptions =
|
||||
options[kWatchItemConfigKeyOptionsInvertProcessExceptions]
|
||||
? [options[kWatchItemConfigKeyOptionsInvertProcessExceptions] boolValue]
|
||||
: kWatchItemPolicyDefaultInvertProcessExceptions;
|
||||
|
||||
std::variant<Unit, ProcessList> proc_list = VerifyConfigWatchItemProcesses(watch_item, err);
|
||||
if (std::holds_alternative<Unit>(proc_list)) {
|
||||
@@ -423,8 +438,8 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
|
||||
for (const PathAndTypePair &path_type_pair : std::get<PathList>(path_list)) {
|
||||
policies.push_back(std::make_shared<WatchItemPolicy>(
|
||||
[name UTF8String], path_type_pair.first, path_type_pair.second, allow_read_access, audit_only,
|
||||
std::get<ProcessList>(proc_list)));
|
||||
NSStringToUTF8StringView(name), path_type_pair.first, path_type_pair.second,
|
||||
allow_read_access, audit_only, invert_process_exceptions, std::get<ProcessList>(proc_list)));
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -645,7 +660,7 @@ void WatchItems::UpdateCurrentState(
|
||||
std::swap(currently_monitored_paths_, new_monitored_paths);
|
||||
current_config_ = new_config;
|
||||
if (new_config) {
|
||||
policy_version_ = [new_config[kWatchItemConfigKeyVersion] UTF8String];
|
||||
policy_version_ = NSStringToUTF8String(new_config[kWatchItemConfigKeyVersion]);
|
||||
} else {
|
||||
policy_version_ = "";
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
using santa::common::Unit;
|
||||
using santa::santad::data_layer::kWatchItemPolicyDefaultAllowReadAccess;
|
||||
using santa::santad::data_layer::kWatchItemPolicyDefaultAuditOnly;
|
||||
using santa::santad::data_layer::kWatchItemPolicyDefaultInvertProcessExceptions;
|
||||
using santa::santad::data_layer::kWatchItemPolicyDefaultPathType;
|
||||
using santa::santad::data_layer::WatchItemPathType;
|
||||
using santa::santad::data_layer::WatchItemPolicy;
|
||||
@@ -777,6 +778,17 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
},
|
||||
policies, &err));
|
||||
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsInvertProcessExceptions : @""}
|
||||
},
|
||||
policies, &err));
|
||||
XCTAssertTrue(ParseConfigSingleWatchItem(@"", @{
|
||||
kWatchItemConfigKeyPaths : @[ @"a" ],
|
||||
kWatchItemConfigKeyOptions : @{kWatchItemConfigKeyOptionsInvertProcessExceptions : @(0)}
|
||||
},
|
||||
policies, &err));
|
||||
|
||||
// If processes are specified, they must be valid format
|
||||
// Note: Full tests in `testVerifyConfigWatchItemProcesses`
|
||||
XCTAssertFalse(ParseConfigSingleWatchItem(
|
||||
@@ -790,9 +802,11 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertTrue(
|
||||
ParseConfigSingleWatchItem(@"rule", @{kWatchItemConfigKeyPaths : @[ @"a" ]}, policies, &err));
|
||||
XCTAssertEqual(policies.size(), 1);
|
||||
XCTAssertEqual(*policies[0].get(), WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType,
|
||||
kWatchItemPolicyDefaultAllowReadAccess,
|
||||
kWatchItemPolicyDefaultAuditOnly, {}));
|
||||
XCTAssertEqual(
|
||||
*policies[0].get(),
|
||||
WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType,
|
||||
kWatchItemPolicyDefaultAllowReadAccess, kWatchItemPolicyDefaultAuditOnly,
|
||||
kWatchItemPolicyDefaultInvertProcessExceptions, {}));
|
||||
|
||||
// Test multiple paths, options, and processes
|
||||
policies.clear();
|
||||
@@ -806,7 +820,8 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
@[ @"a", @{kWatchItemConfigKeyPathsPath : @"b", kWatchItemConfigKeyPathsIsPrefix : @(YES)} ],
|
||||
kWatchItemConfigKeyOptions : @{
|
||||
kWatchItemConfigKeyOptionsAllowReadAccess : @(YES),
|
||||
kWatchItemConfigKeyOptionsAuditOnly : @(NO)
|
||||
kWatchItemConfigKeyOptionsAuditOnly : @(NO),
|
||||
kWatchItemConfigKeyOptionsInvertProcessExceptions : @(YES),
|
||||
},
|
||||
kWatchItemConfigKeyProcesses : @[
|
||||
@{kWatchItemConfigKeyProcessesBinaryPath : @"pa"},
|
||||
@@ -815,10 +830,10 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
},
|
||||
policies, &err));
|
||||
XCTAssertEqual(policies.size(), 2);
|
||||
XCTAssertEqual(*policies[0].get(),
|
||||
WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType, true, false, procs));
|
||||
XCTAssertEqual(*policies[1].get(),
|
||||
WatchItemPolicy("rule", "b", WatchItemPathType::kPrefix, true, false, procs));
|
||||
XCTAssertEqual(*policies[0].get(), WatchItemPolicy("rule", "a", kWatchItemPolicyDefaultPathType,
|
||||
true, false, true, procs));
|
||||
XCTAssertEqual(*policies[1].get(), WatchItemPolicy("rule", "b", WatchItemPathType::kPrefix, true,
|
||||
false, true, procs));
|
||||
}
|
||||
|
||||
- (void)testState {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#include "Source/common/SantaCache.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
@@ -33,16 +34,29 @@ enum class FlushCacheMode {
|
||||
kAllCaches,
|
||||
};
|
||||
|
||||
enum class FlushCacheReason {
|
||||
kClientModeChanged,
|
||||
kPathRegexChanged,
|
||||
kRulesChanged,
|
||||
kStaticRulesChanged,
|
||||
kExplicitCommand,
|
||||
kFilesystemUnmounted,
|
||||
};
|
||||
|
||||
class AuthResultCache {
|
||||
public:
|
||||
// Santa currently only flushes caches when new DENY rules are added, not
|
||||
// ALLOW rules. This means this value should be low enough so that if a
|
||||
// ALLOW rules. This means cache_deny_time_ms should be low enough so that if a
|
||||
// previously denied binary is allowed, it can be re-executed by the user in a
|
||||
// timely manner. But the value should be high enough to allow the cache to be
|
||||
// effective in the event the binary is executed in rapid succession.
|
||||
static std::unique_ptr<AuthResultCache> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
SNTMetricSet *metric_set, uint64_t cache_deny_time_ms = 1500);
|
||||
|
||||
AuthResultCache(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms = 1500);
|
||||
SNTMetricCounter *flush_count, uint64_t cache_deny_time_ms = 1500);
|
||||
virtual ~AuthResultCache();
|
||||
|
||||
AuthResultCache(AuthResultCache &&other) = delete;
|
||||
@@ -55,7 +69,7 @@ class AuthResultCache {
|
||||
virtual SNTAction CheckCache(const es_file_t *es_file);
|
||||
virtual SNTAction CheckCache(SantaVnode vnode_id);
|
||||
|
||||
virtual void FlushCache(FlushCacheMode mode);
|
||||
virtual void FlushCache(FlushCacheMode mode, FlushCacheReason reason);
|
||||
|
||||
virtual NSArray<NSNumber *> *CacheCounts();
|
||||
|
||||
@@ -66,6 +80,7 @@ class AuthResultCache {
|
||||
SantaCache<SantaVnode, uint64_t> *nonroot_cache_;
|
||||
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
|
||||
SNTMetricCounter *flush_count_;
|
||||
uint64_t root_devno_;
|
||||
uint64_t cache_deny_time_ns_;
|
||||
dispatch_queue_t q_;
|
||||
|
||||
@@ -25,6 +25,13 @@
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
|
||||
static NSString *const kFlushCacheReasonClientModeChanged = @"ClientModeChanged";
|
||||
static NSString *const kFlushCacheReasonPathRegexChanged = @"PathRegexChanged";
|
||||
static NSString *const kFlushCacheReasonRulesChanged = @"RulesChanged";
|
||||
static NSString *const kFlushCacheReasonStaticRulesChanged = @"StaticRulesChanged";
|
||||
static NSString *const kFlushCacheReasonExplicitCommand = @"ExplicitCommand";
|
||||
static NSString *const kFlushCacheReasonFilesystemUnmounted = @"FilesystemUnmounted";
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
static inline uint64_t GetCurrentUptime() {
|
||||
@@ -44,9 +51,36 @@ static inline uint64_t TimestampFromCachedValue(uint64_t cachedValue) {
|
||||
return (cachedValue & ~(0xFF00000000000000));
|
||||
}
|
||||
|
||||
NSString *const FlushCacheReasonToString(FlushCacheReason reason) {
|
||||
switch (reason) {
|
||||
case FlushCacheReason::kClientModeChanged: return kFlushCacheReasonClientModeChanged;
|
||||
case FlushCacheReason::kPathRegexChanged: return kFlushCacheReasonPathRegexChanged;
|
||||
case FlushCacheReason::kRulesChanged: return kFlushCacheReasonRulesChanged;
|
||||
case FlushCacheReason::kStaticRulesChanged: return kFlushCacheReasonStaticRulesChanged;
|
||||
case FlushCacheReason::kExplicitCommand: return kFlushCacheReasonExplicitCommand;
|
||||
case FlushCacheReason::kFilesystemUnmounted: return kFlushCacheReasonFilesystemUnmounted;
|
||||
default:
|
||||
[NSException raise:@"Invalid reason" format:@"Unknown reason value: %d", reason];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<AuthResultCache> AuthResultCache::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTMetricSet *metric_set,
|
||||
uint64_t cache_deny_time_ms) {
|
||||
SNTMetricCounter *flush_count =
|
||||
[metric_set counterWithName:@"/santa/flush_count"
|
||||
fieldNames:@[ @"Reason" ]
|
||||
helpText:@"Count of times the auth result cache is flushed by reason"];
|
||||
|
||||
return std::make_unique<AuthResultCache>(esapi, flush_count, cache_deny_time_ms);
|
||||
}
|
||||
|
||||
AuthResultCache::AuthResultCache(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms)
|
||||
: esapi_(esapi), cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
|
||||
SNTMetricCounter *flush_count, uint64_t cache_deny_time_ms)
|
||||
: esapi_(esapi),
|
||||
flush_count_(flush_count),
|
||||
cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
|
||||
root_cache_ = new SantaCache<SantaVnode, uint64_t>();
|
||||
nonroot_cache_ = new SantaCache<SantaVnode, uint64_t>();
|
||||
|
||||
@@ -118,7 +152,7 @@ SantaCache<SantaVnode, uint64_t> *AuthResultCache::CacheForVnodeID(SantaVnode vn
|
||||
return (vnode_id.fsid == root_devno_ || root_devno_ == 0) ? root_cache_ : nonroot_cache_;
|
||||
}
|
||||
|
||||
void AuthResultCache::FlushCache(FlushCacheMode mode) {
|
||||
void AuthResultCache::FlushCache(FlushCacheMode mode, FlushCacheReason reason) {
|
||||
nonroot_cache_->clear();
|
||||
if (mode == FlushCacheMode::kAllCaches) {
|
||||
root_cache_->clear();
|
||||
@@ -134,6 +168,8 @@ void AuthResultCache::FlushCache(FlushCacheMode mode) {
|
||||
shared_esapi->ClearCache(Client());
|
||||
});
|
||||
}
|
||||
|
||||
[flush_count_ incrementForFieldValues:@[ FlushCacheReasonToString(reason) ]];
|
||||
}
|
||||
|
||||
NSArray<NSNumber *> *AuthResultCache::CacheCounts() {
|
||||
|
||||
@@ -29,6 +29,13 @@
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::FlushCacheReason;
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
extern NSString *const FlushCacheReasonToString(FlushCacheReason reason);
|
||||
}
|
||||
|
||||
using santa::santad::event_providers::FlushCacheReasonToString;
|
||||
|
||||
// Grab the st_dev number of the root volume to match the root cache
|
||||
static uint64_t RootDevno() {
|
||||
@@ -66,14 +73,14 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
|
||||
- (void)testEmptyCacheExpectedNumberOfCacheCounts {
|
||||
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(esapi);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(esapi, nil);
|
||||
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
- (void)testBasicOperation {
|
||||
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(esapi);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(esapi, nil);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 222);
|
||||
@@ -110,7 +117,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
|
||||
- (void)testFlushCache {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(mockESApi, nil);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 111);
|
||||
@@ -121,7 +128,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
|
||||
// Flush non-root only
|
||||
cache->FlushCache(FlushCacheMode::kNonRootOnly);
|
||||
cache->FlushCache(FlushCacheMode::kNonRootOnly, FlushCacheReason::kClientModeChanged);
|
||||
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
|
||||
@@ -138,7 +145,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
dispatch_semaphore_signal(sema);
|
||||
return true;
|
||||
}));
|
||||
cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
cache->FlushCache(FlushCacheMode::kAllCaches, FlushCacheReason::kClientModeChanged);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
|
||||
@@ -151,7 +158,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
|
||||
- (void)testCacheStateMachine {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(mockESApi, nil);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
@@ -193,7 +200,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
// Create a cache with a lowered cache expiry value
|
||||
uint64_t expiryMS = 250;
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi, expiryMS);
|
||||
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(mockESApi, nil, expiryMS);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
@@ -215,4 +222,21 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
- (void)testFlushCacheReasonToString {
|
||||
std::map<FlushCacheReason, NSString *> reasonToString = {
|
||||
{FlushCacheReason::kClientModeChanged, @"ClientModeChanged"},
|
||||
{FlushCacheReason::kPathRegexChanged, @"PathRegexChanged"},
|
||||
{FlushCacheReason::kRulesChanged, @"RulesChanged"},
|
||||
{FlushCacheReason::kStaticRulesChanged, @"StaticRulesChanged"},
|
||||
{FlushCacheReason::kExplicitCommand, @"ExplicitCommand"},
|
||||
{FlushCacheReason::kFilesystemUnmounted, @"FilesystemUnmounted"},
|
||||
};
|
||||
|
||||
for (const auto &kv : reasonToString) {
|
||||
XCTAssertEqualObjects(FlushCacheReasonToString(kv.first), kv.second);
|
||||
}
|
||||
|
||||
XCTAssertThrows(FlushCacheReasonToString((FlushCacheReason)12345));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -46,7 +46,11 @@ class EnrichedFile {
|
||||
group_(std::move(other.group_)),
|
||||
hash_(std::move(other.hash_)) {}
|
||||
|
||||
// Note: Move assignment could be safely implemented but not currently needed
|
||||
EnrichedFile &operator=(EnrichedFile &&other) = delete;
|
||||
|
||||
EnrichedFile(const EnrichedFile &other) = delete;
|
||||
EnrichedFile &operator=(const EnrichedFile &other) = delete;
|
||||
|
||||
const std::optional<std::shared_ptr<std::string>> &user() const {
|
||||
return user_;
|
||||
@@ -87,7 +91,11 @@ class EnrichedProcess {
|
||||
real_group_(std::move(other.real_group_)),
|
||||
executable_(std::move(other.executable_)) {}
|
||||
|
||||
// Note: Move assignment could be safely implemented but not currently needed
|
||||
EnrichedProcess &operator=(EnrichedProcess &&other) = delete;
|
||||
|
||||
EnrichedProcess(const EnrichedProcess &other) = delete;
|
||||
EnrichedProcess &operator=(const EnrichedProcess &other) = delete;
|
||||
|
||||
const std::optional<std::shared_ptr<std::string>> &effective_user() const {
|
||||
return effective_user_;
|
||||
@@ -123,7 +131,12 @@ class EnrichedEventType {
|
||||
instigator_(std::move(other.instigator_)),
|
||||
enrichment_time_(std::move(other.enrichment_time_)) {}
|
||||
|
||||
// Note: Move assignment could be safely implemented but not currently needed
|
||||
// so no sense in implementing across all child classes
|
||||
EnrichedEventType &operator=(EnrichedEventType &&other) = delete;
|
||||
|
||||
EnrichedEventType(const EnrichedEventType &other) = delete;
|
||||
EnrichedEventType &operator=(const EnrichedEventType &other) = delete;
|
||||
|
||||
virtual ~EnrichedEventType() = default;
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class Enricher {
|
||||
public:
|
||||
Enricher();
|
||||
virtual ~Enricher() = default;
|
||||
virtual std::shared_ptr<EnrichedMessage> Enrich(Message &&msg);
|
||||
virtual std::unique_ptr<EnrichedMessage> Enrich(Message &&msg);
|
||||
virtual EnrichedProcess Enrich(
|
||||
const es_process_t &es_proc,
|
||||
EnrichOptions options = EnrichOptions::kDefault);
|
||||
|
||||
@@ -30,19 +30,19 @@ namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Enricher::Enricher() : username_cache_(256), groupname_cache_(256) {}
|
||||
|
||||
std::shared_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
|
||||
std::unique_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
|
||||
// TODO(mlw): Consider potential design patterns that could help reduce memory usage under load
|
||||
// (such as maybe the flyweight pattern)
|
||||
switch (es_msg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedClose(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedClose(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.close.target)));
|
||||
case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedExchange(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedExchange(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.exchangedata.file1),
|
||||
Enrich(*es_msg->event.exchangedata.file2)));
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedExec(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedExec(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.exec.target),
|
||||
(es_msg->version >= 2 && es_msg->event.exec.script)
|
||||
? std::make_optional(Enrich(*es_msg->event.exec.script))
|
||||
@@ -51,28 +51,28 @@ std::shared_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
|
||||
? std::make_optional(Enrich(*es_msg->event.exec.cwd))
|
||||
: std::nullopt));
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedFork(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedFork(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.fork.child)));
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT:
|
||||
return std::make_shared<EnrichedMessage>(
|
||||
return std::make_unique<EnrichedMessage>(
|
||||
EnrichedExit(std::move(es_msg), Enrich(*es_msg->process)));
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK:
|
||||
return std::make_shared<EnrichedMessage>(
|
||||
return std::make_unique<EnrichedMessage>(
|
||||
EnrichedLink(std::move(es_msg), Enrich(*es_msg->process),
|
||||
Enrich(*es_msg->event.link.source), Enrich(*es_msg->event.link.target_dir)));
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: {
|
||||
if (es_msg->event.rename.destination_type == ES_DESTINATION_TYPE_NEW_PATH) {
|
||||
return std::make_shared<EnrichedMessage>(EnrichedRename(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedRename(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.rename.source),
|
||||
std::nullopt, Enrich(*es_msg->event.rename.destination.new_path.dir)));
|
||||
} else {
|
||||
return std::make_shared<EnrichedMessage>(EnrichedRename(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedRename(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.rename.source),
|
||||
Enrich(*es_msg->event.rename.destination.existing_file), std::nullopt));
|
||||
}
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedUnlink(
|
||||
return std::make_unique<EnrichedMessage>(EnrichedUnlink(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.unlink.target)));
|
||||
default:
|
||||
// This is a programming error
|
||||
|
||||
@@ -234,7 +234,7 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
EXPECT_CALL(*mockAuthCache, CheckCache)
|
||||
.WillOnce(testing::Return(SNTActionRequestBinary))
|
||||
.WillOnce(testing::Return(SNTActionRequestBinary))
|
||||
@@ -301,7 +301,7 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, SNTActionRespondAllowCompiler))
|
||||
.WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, SNTActionRespondAllow))
|
||||
|
||||
@@ -193,13 +193,12 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
return _esApi->UnsubscribeAll(_esClient);
|
||||
}
|
||||
|
||||
- (bool)unmuteEverything {
|
||||
bool result = _esApi->UnmuteAllPaths(_esClient);
|
||||
result = _esApi->UnmuteAllTargetPaths(_esClient) && result;
|
||||
return result;
|
||||
- (bool)unmuteAllTargetPaths {
|
||||
return _esApi->UnmuteAllTargetPaths(_esClient);
|
||||
}
|
||||
|
||||
- (bool)enableTargetPathWatching {
|
||||
[self unmuteAllTargetPaths];
|
||||
return _esApi->InvertTargetPathMuting(_esClient);
|
||||
}
|
||||
|
||||
@@ -236,9 +235,9 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
}
|
||||
}
|
||||
|
||||
- (void)processEnrichedMessage:(std::shared_ptr<EnrichedMessage>)msg
|
||||
handler:(void (^)(std::shared_ptr<EnrichedMessage>))messageHandler {
|
||||
__block std::shared_ptr<EnrichedMessage> msgTmp = std::move(msg);
|
||||
- (void)processEnrichedMessage:(std::unique_ptr<EnrichedMessage>)msg
|
||||
handler:(void (^)(std::unique_ptr<EnrichedMessage>))messageHandler {
|
||||
__block std::unique_ptr<EnrichedMessage> msgTmp = std::move(msg);
|
||||
dispatch_async(_notifyQueue, ^{
|
||||
messageHandler(std::move(msgTmp));
|
||||
});
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
- (bool)subscribeAndClearCache:(const std::set<es_event_type_t> &)events;
|
||||
|
||||
- (bool)unsubscribeAll;
|
||||
- (bool)unmuteEverything;
|
||||
- (bool)unmuteAllTargetPaths;
|
||||
- (bool)enableTargetPathWatching;
|
||||
- (bool)muteTargetPaths:
|
||||
(const std::vector<std::pair<std::string, santa::santad::data_layer::WatchItemPathType>> &)paths;
|
||||
@@ -72,9 +72,9 @@
|
||||
|
||||
- (void)
|
||||
processEnrichedMessage:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage>)msg
|
||||
(std::unique_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage>)msg
|
||||
handler:
|
||||
(void (^)(std::shared_ptr<
|
||||
(void (^)(std::unique_ptr<
|
||||
santa::santad::event_providers::endpoint_security::EnrichedMessage>))
|
||||
messageHandler;
|
||||
|
||||
|
||||
@@ -274,23 +274,20 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testUnmuteEverything {
|
||||
- (void)testUnmuteAllTargetPaths {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
// Test variations of underlying unmute impls returning both true and false
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllPaths)
|
||||
.WillOnce(testing::Return(true))
|
||||
.WillOnce(testing::Return(false));
|
||||
// Test the underlying unmute impl returning both true and false
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths)
|
||||
.WillOnce(testing::Return(true))
|
||||
.WillOnce(testing::Return(true));
|
||||
.WillOnce(testing::Return(false));
|
||||
|
||||
XCTAssertTrue([client unmuteEverything]);
|
||||
XCTAssertFalse([client unmuteEverything]);
|
||||
XCTAssertTrue([client unmuteAllTargetPaths]);
|
||||
XCTAssertFalse([client unmuteAllTargetPaths]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
@@ -302,6 +299,9 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
// UnmuteAllTargetPaths is always attempted.
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).Times(2).WillRepeatedly(testing::Return(true));
|
||||
|
||||
// Test the underlying invert nute impl returning both true and false
|
||||
EXPECT_CALL(*mockESApi, InvertTargetPathMuting)
|
||||
.WillOnce(testing::Return(true))
|
||||
@@ -406,14 +406,21 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
{
|
||||
auto enrichedMsg = std::make_shared<EnrichedMessage>(
|
||||
auto enrichedMsg = std::make_unique<EnrichedMessage>(
|
||||
EnrichedClose(Message(mockESApi, &esMsg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
|
||||
[client processEnrichedMessage:enrichedMsg
|
||||
handler:^(std::shared_ptr<EnrichedMessage> msg) {
|
||||
[client processEnrichedMessage:std::move(enrichedMsg)
|
||||
handler:^(std::unique_ptr<EnrichedMessage> msg) {
|
||||
// reset the shared_ptr to drop the held message.
|
||||
// This is a workaround for a TSAN only false positive
|
||||
// which happens if we switch back to the sem wait
|
||||
// after signaling, but _before_ the implicit release
|
||||
// of msg. In that case, the mock verify and the
|
||||
// call of the mock's Release method can data race.
|
||||
msg.reset();
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::FlushCacheReason;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
@@ -172,6 +173,15 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
- (void)handleMessage:(Message &&)esMsg
|
||||
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
|
||||
// Process the unmount event first so that caches are flushed before any
|
||||
// other potential early returns.
|
||||
if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_UNMOUNT) {
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kNonRootOnly,
|
||||
FlushCacheReason::kFilesystemUnmounted);
|
||||
recordEventMetrics(EventDisposition::kProcessed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.blockUSBMount) {
|
||||
// TODO: We should also unsubscribe from events when this isn't set, but
|
||||
// this is generally a low-volume event type.
|
||||
@@ -180,12 +190,6 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return;
|
||||
}
|
||||
|
||||
if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_UNMOUNT) {
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kNonRootOnly);
|
||||
recordEventMetrics(EventDisposition::kProcessed);
|
||||
return;
|
||||
}
|
||||
|
||||
[self processMessage:std::move(esMsg)
|
||||
handler:^(const Message &msg) {
|
||||
es_auth_result_t result = [self handleAuthMount:msg];
|
||||
|
||||
@@ -39,13 +39,14 @@
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::FlushCacheReason;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
class MockAuthResultCache : public AuthResultCache {
|
||||
public:
|
||||
using AuthResultCache::AuthResultCache;
|
||||
|
||||
MOCK_METHOD(void, FlushCache, (FlushCacheMode mode));
|
||||
MOCK_METHOD(void, FlushCache, (FlushCacheMode mode, FlushCacheReason reason));
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManager (Testing)
|
||||
@@ -316,7 +317,7 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
EXPECT_CALL(*mockAuthCache, FlushCache);
|
||||
|
||||
SNTEndpointSecurityDeviceManager *deviceManager =
|
||||
|
||||
@@ -241,7 +241,6 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
[self establishClientOrDie];
|
||||
|
||||
[super enableTargetPathWatching];
|
||||
[super unmuteEverything];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -426,19 +425,35 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
return specialCase;
|
||||
}
|
||||
|
||||
FileAccessPolicyDecision decision = FileAccessPolicyDecision::kDenied;
|
||||
|
||||
for (const WatchItemPolicy::Process &process : policy->processes) {
|
||||
if ([self policyProcess:process matchesESProcess:msg->process]) {
|
||||
return FileAccessPolicyDecision::kAllowed;
|
||||
decision = FileAccessPolicyDecision::kAllowed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (policy->audit_only) {
|
||||
return FileAccessPolicyDecision::kAllowedAuditOnly;
|
||||
} else {
|
||||
// TODO(xyz): Write to TTY like in exec controller?
|
||||
// TODO(xyz): Need new config item for custom message in UI
|
||||
return FileAccessPolicyDecision::kDenied;
|
||||
// If the `invert_process_exceptions` option is set, the decision should be
|
||||
// inverted from allowed to denied or vice versa. Note that this inversion
|
||||
// must be made prior to checking the policy's audit-only flag.
|
||||
if (policy->invert_process_exceptions) {
|
||||
if (decision == FileAccessPolicyDecision::kAllowed) {
|
||||
decision = FileAccessPolicyDecision::kDenied;
|
||||
} else {
|
||||
decision = FileAccessPolicyDecision::kAllowed;
|
||||
}
|
||||
}
|
||||
|
||||
if (decision == FileAccessPolicyDecision::kDenied && policy->audit_only) {
|
||||
decision = FileAccessPolicyDecision::kAllowedAuditOnly;
|
||||
}
|
||||
|
||||
// https://github.com/google/santa/issues/1084
|
||||
// TODO(xyz): Write to TTY like in exec controller?
|
||||
// TODO(xyz): Need new config item for custom message in UI
|
||||
|
||||
return decision;
|
||||
}
|
||||
|
||||
- (FileAccessPolicyDecision)handleMessage:(const Message &)msg
|
||||
@@ -554,7 +569,7 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
if ([super unsubscribeAll]) {
|
||||
self.isSubscribed = false;
|
||||
}
|
||||
[super unmuteEverything];
|
||||
[super unmuteAllTargetPaths];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ extern es_auth_result_t CombinePolicyResults(es_auth_result_t result1, es_auth_r
|
||||
void SetExpectationsForFileAccessAuthorizerInit(
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi) {
|
||||
EXPECT_CALL(*mockESApi, InvertTargetPathMuting).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllPaths).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).WillOnce(testing::Return(true));
|
||||
}
|
||||
|
||||
@@ -532,8 +531,9 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
|
||||
// If no policy exists, the operation is allowed
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertEqual([accessClient applyPolicy:std::nullopt forTarget:target toMessage:msg],
|
||||
XCTAssertEqual([accessClient applyPolicy:std::nullopt
|
||||
forTarget:target
|
||||
toMessage:Message(mockESApi, &esMsg)],
|
||||
FileAccessPolicyDecision::kNoPolicy);
|
||||
}
|
||||
|
||||
@@ -546,8 +546,9 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
{
|
||||
OCMExpect([self.mockConfigurator enableBadSignatureProtection]).andReturn(YES);
|
||||
esMsg.process->codesigning_flags = CS_SIGNED;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy forTarget:target toMessage:msg],
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:Message(mockESApi, &esMsg)],
|
||||
FileAccessPolicyDecision::kDeniedInvalidSignature);
|
||||
}
|
||||
|
||||
@@ -557,11 +558,12 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
{
|
||||
OCMExpect([self.mockConfigurator enableBadSignatureProtection]).andReturn(NO);
|
||||
esMsg.process->codesigning_flags = CS_SIGNED;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
OCMExpect([accessClientMock policyProcess:policyProc matchesESProcess:&esProc])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(true);
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy forTarget:target toMessage:msg],
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:Message(mockESApi, &esMsg)],
|
||||
FileAccessPolicyDecision::kAllowed);
|
||||
}
|
||||
|
||||
@@ -574,8 +576,9 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(false);
|
||||
policy->audit_only = false;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy forTarget:target toMessage:msg],
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:Message(mockESApi, &esMsg)],
|
||||
FileAccessPolicyDecision::kDenied);
|
||||
}
|
||||
|
||||
@@ -585,8 +588,50 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(false);
|
||||
policy->audit_only = true;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy forTarget:target toMessage:msg],
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:Message(mockESApi, &esMsg)],
|
||||
FileAccessPolicyDecision::kAllowedAuditOnly);
|
||||
}
|
||||
|
||||
// The remainder of the tests set the policy's `invert_process_exceptions` option
|
||||
policy->invert_process_exceptions = true;
|
||||
|
||||
// If no exceptions for inverted policy, operations are allowed
|
||||
{
|
||||
OCMExpect([accessClientMock policyProcess:policyProc matchesESProcess:&esProc])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(false);
|
||||
policy->audit_only = false;
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:Message(mockESApi, &esMsg)],
|
||||
FileAccessPolicyDecision::kAllowed);
|
||||
}
|
||||
|
||||
// For audit only policies with no exception matches and inverted exceptions, operations are
|
||||
// allowed
|
||||
{
|
||||
OCMExpect([accessClientMock policyProcess:policyProc matchesESProcess:&esProc])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(false);
|
||||
policy->audit_only = true;
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:Message(mockESApi, &esMsg)],
|
||||
FileAccessPolicyDecision::kAllowed);
|
||||
}
|
||||
|
||||
// For audit only policies with exception match and inverted exceptions, operations are allowed
|
||||
// audit only
|
||||
{
|
||||
OCMExpect([accessClientMock policyProcess:policyProc matchesESProcess:&esProc])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(true);
|
||||
policy->audit_only = true;
|
||||
XCTAssertEqual([accessClient applyPolicy:optionalPolicy
|
||||
forTarget:target
|
||||
toMessage:Message(mockESApi, &esMsg)],
|
||||
FileAccessPolicyDecision::kAllowedAuditOnly);
|
||||
}
|
||||
|
||||
@@ -636,7 +681,6 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
decisionCache:nil];
|
||||
|
||||
EXPECT_CALL(*mockESApi, UnsubscribeAll);
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllPaths).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).WillOnce(testing::Return(true));
|
||||
|
||||
accessClient.isSubscribed = true;
|
||||
|
||||
@@ -110,11 +110,11 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
|
||||
// Enrich the message inline with the ES handler block to capture enrichment
|
||||
// data as close to the source event as possible.
|
||||
std::shared_ptr<EnrichedMessage> sharedEnrichedMessage = _enricher->Enrich(std::move(esMsg));
|
||||
std::unique_ptr<EnrichedMessage> enrichedMessage = _enricher->Enrich(std::move(esMsg));
|
||||
|
||||
// Asynchronously log the message
|
||||
[self processEnrichedMessage:std::move(sharedEnrichedMessage)
|
||||
handler:^(std::shared_ptr<EnrichedMessage> msg) {
|
||||
[self processEnrichedMessage:std::move(enrichedMessage)
|
||||
handler:^(std::unique_ptr<EnrichedMessage> msg) {
|
||||
self->_logger->Log(std::move(msg));
|
||||
recordEventMetrics(EventDisposition::kProcessed);
|
||||
}];
|
||||
|
||||
@@ -48,7 +48,7 @@ using santa::santad::logs::endpoint_security::Logger;
|
||||
|
||||
class MockEnricher : public Enricher {
|
||||
public:
|
||||
MOCK_METHOD(std::shared_ptr<EnrichedMessage>, Enrich, (Message &&));
|
||||
MOCK_METHOD(std::unique_ptr<EnrichedMessage>, Enrich, (Message &&));
|
||||
};
|
||||
|
||||
class MockAuthResultCache : public AuthResultCache {
|
||||
@@ -62,7 +62,7 @@ class MockLogger : public Logger {
|
||||
public:
|
||||
using Logger::Logger;
|
||||
|
||||
MOCK_METHOD(void, Log, (std::shared_ptr<EnrichedMessage>));
|
||||
MOCK_METHOD(void, Log, (std::unique_ptr<EnrichedMessage>));
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityRecorderTest : XCTestCase
|
||||
@@ -100,12 +100,12 @@ class MockLogger : public Logger {
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
std::shared_ptr<EnrichedMessage> enrichedMsg = std::shared_ptr<EnrichedMessage>(nullptr);
|
||||
std::unique_ptr<EnrichedMessage> enrichedMsg = std::unique_ptr<EnrichedMessage>(nullptr);
|
||||
|
||||
auto mockEnricher = std::make_shared<MockEnricher>();
|
||||
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(enrichedMsg));
|
||||
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(std::move(enrichedMsg)));
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFile)).Times(1);
|
||||
|
||||
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
|
||||
|
||||
@@ -111,7 +111,6 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
|
||||
|
||||
- (void)enable {
|
||||
[super enableTargetPathWatching];
|
||||
[super unmuteEverything];
|
||||
|
||||
// Get the set of protected paths
|
||||
std::set<std::string> protectedPaths = [SNTEndpointSecurityTamperResistance getProtectedPaths];
|
||||
|
||||
@@ -65,7 +65,6 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
|
||||
|
||||
// Setup mocks to handle inverting target path muting
|
||||
EXPECT_CALL(*mockESApi, InvertTargetPathMuting).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllPaths).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteAllTargetPaths).WillOnce(testing::Return(true));
|
||||
|
||||
// Setup mocks to handle muting the rules db and events db
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
// Forward declarations
|
||||
@class SNTStoredEvent;
|
||||
@@ -39,8 +40,8 @@ class Logger {
|
||||
public:
|
||||
static std::unique_ptr<Logger> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
SNTEventLogType log_type, NSString *event_log_path, NSString *spool_log_path,
|
||||
size_t spool_dir_size_threshold, size_t spool_file_size_threshold,
|
||||
SNTEventLogType log_type, SNTDecisionCache *decision_cache, NSString *event_log_path,
|
||||
NSString *spool_log_path, size_t spool_dir_size_threshold, size_t spool_file_size_threshold,
|
||||
uint64_t spool_flush_timeout_ms);
|
||||
|
||||
Logger(std::shared_ptr<serializers::Serializer> serializer,
|
||||
@@ -49,7 +50,7 @@ class Logger {
|
||||
virtual ~Logger() = default;
|
||||
|
||||
virtual void Log(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage> msg);
|
||||
std::unique_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage> msg);
|
||||
|
||||
void LogAllowlist(const santa::santad::event_providers::endpoint_security::Message &msg,
|
||||
const std::string_view hash);
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/File.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Null.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Spool.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Syslog.h"
|
||||
#include "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
@@ -48,24 +50,26 @@ static const size_t kMaxExpectedWriteSizeBytes = 4096;
|
||||
|
||||
// Translate configured log type to appropriate Serializer/Writer pairs
|
||||
std::unique_ptr<Logger> Logger::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTEventLogType log_type, NSString *event_log_path,
|
||||
NSString *spool_log_path, size_t spool_dir_size_threshold,
|
||||
SNTEventLogType log_type, SNTDecisionCache *decision_cache,
|
||||
NSString *event_log_path, NSString *spool_log_path,
|
||||
size_t spool_dir_size_threshold,
|
||||
size_t spool_file_size_threshold,
|
||||
uint64_t spool_flush_timeout_ms) {
|
||||
switch (log_type) {
|
||||
case SNTEventLogTypeFilelog:
|
||||
return std::make_unique<Logger>(
|
||||
BasicString::Create(esapi),
|
||||
BasicString::Create(esapi, std::move(decision_cache)),
|
||||
File::Create(event_log_path, kFlushBufferTimeoutMS, kBufferBatchSizeBytes,
|
||||
kMaxExpectedWriteSizeBytes));
|
||||
case SNTEventLogTypeSyslog:
|
||||
return std::make_unique<Logger>(BasicString::Create(esapi, false), Syslog::Create());
|
||||
return std::make_unique<Logger>(BasicString::Create(esapi, std::move(decision_cache), false),
|
||||
Syslog::Create());
|
||||
case SNTEventLogTypeNull: return std::make_unique<Logger>(Empty::Create(), Null::Create());
|
||||
case SNTEventLogTypeProtobuf:
|
||||
LOGW(@"The EventLogType value protobuf is currently in beta. The protobuf schema is subject "
|
||||
@"to change.");
|
||||
return std::make_unique<Logger>(
|
||||
Protobuf::Create(esapi),
|
||||
Protobuf::Create(esapi, std::move(decision_cache)),
|
||||
Spool::Create([spool_log_path UTF8String], spool_dir_size_threshold,
|
||||
spool_file_size_threshold, spool_flush_timeout_ms));
|
||||
default: LOGE(@"Invalid log type: %ld", log_type); return nullptr;
|
||||
@@ -76,7 +80,7 @@ Logger::Logger(std::shared_ptr<serializers::Serializer> serializer,
|
||||
std::shared_ptr<writers::Writer> writer)
|
||||
: serializer_(std::move(serializer)), writer_(std::move(writer)) {}
|
||||
|
||||
void Logger::Log(std::shared_ptr<EnrichedMessage> msg) {
|
||||
void Logger::Log(std::unique_ptr<EnrichedMessage> msg) {
|
||||
writer_->Write(serializer_->SerializeMessage(std::move(msg)));
|
||||
}
|
||||
|
||||
|
||||
@@ -103,26 +103,26 @@ class MockWriter : public Null {
|
||||
// Ensure that the factory method creates expected serializers/writers pairs
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
XCTAssertEqual(nullptr, Logger::Create(mockESApi, (SNTEventLogType)123, @"/tmp/temppy",
|
||||
XCTAssertEqual(nullptr, Logger::Create(mockESApi, (SNTEventLogType)123, nil, @"/tmp/temppy",
|
||||
@"/tmp/spool", 1, 1, 1));
|
||||
|
||||
LoggerPeer logger(
|
||||
Logger::Create(mockESApi, SNTEventLogTypeFilelog, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
Logger::Create(mockESApi, SNTEventLogTypeFilelog, nil, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<BasicString>(logger.Serializer()));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<File>(logger.Writer()));
|
||||
|
||||
logger = LoggerPeer(
|
||||
Logger::Create(mockESApi, SNTEventLogTypeSyslog, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
Logger::Create(mockESApi, SNTEventLogTypeSyslog, nil, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<BasicString>(logger.Serializer()));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Syslog>(logger.Writer()));
|
||||
|
||||
logger = LoggerPeer(
|
||||
Logger::Create(mockESApi, SNTEventLogTypeNull, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
Logger::Create(mockESApi, SNTEventLogTypeNull, nil, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Empty>(logger.Serializer()));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Null>(logger.Writer()));
|
||||
|
||||
logger = LoggerPeer(
|
||||
Logger::Create(mockESApi, SNTEventLogTypeProtobuf, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
logger = LoggerPeer(Logger::Create(mockESApi, SNTEventLogTypeProtobuf, nil, @"/tmp/temppy",
|
||||
@"/tmp/spool", 1, 1, 1));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Protobuf>(logger.Serializer()));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Spool>(logger.Writer()));
|
||||
}
|
||||
@@ -136,16 +136,19 @@ class MockWriter : public Null {
|
||||
es_message_t msg;
|
||||
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
auto enrichedMsg = std::make_shared<EnrichedMessage>(
|
||||
EnrichedClose(Message(mockESApi, &msg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
|
||||
EXPECT_CALL(*mockSerializer, SerializeMessage(testing::A<const EnrichedClose &>())).Times(1);
|
||||
EXPECT_CALL(*mockWriter, Write).Times(1);
|
||||
{
|
||||
auto enrichedMsg = std::make_unique<EnrichedMessage>(
|
||||
EnrichedClose(Message(mockESApi, &msg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
|
||||
Logger(mockSerializer, mockWriter).Log(enrichedMsg);
|
||||
EXPECT_CALL(*mockSerializer, SerializeMessage(testing::A<const EnrichedClose &>())).Times(1);
|
||||
EXPECT_CALL(*mockWriter, Write).Times(1);
|
||||
|
||||
Logger(mockSerializer, mockWriter).Log(std::move(enrichedMsg));
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
@@ -31,11 +32,11 @@ class BasicString : public Serializer {
|
||||
public:
|
||||
static std::shared_ptr<BasicString> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
bool prefix_time_name = true);
|
||||
SNTDecisionCache *decision_cache, bool prefix_time_name = true);
|
||||
|
||||
BasicString(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
bool prefix_time_name);
|
||||
SNTDecisionCache *decision_cache, bool prefix_time_name);
|
||||
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
#include <string>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h"
|
||||
@@ -92,11 +91,13 @@ std::string GetReasonString(SNTEventState event_state) {
|
||||
case SNTEventStateAllowCertificate: return "CERT";
|
||||
case SNTEventStateAllowScope: return "SCOPE";
|
||||
case SNTEventStateAllowTeamID: return "TEAMID";
|
||||
case SNTEventStateAllowSigningID: return "SIGNINGID";
|
||||
case SNTEventStateAllowUnknown: return "UNKNOWN";
|
||||
case SNTEventStateBlockBinary: return "BINARY";
|
||||
case SNTEventStateBlockCertificate: return "CERT";
|
||||
case SNTEventStateBlockScope: return "SCOPE";
|
||||
case SNTEventStateBlockTeamID: return "TEAMID";
|
||||
case SNTEventStateBlockSigningID: return "SIGNINGID";
|
||||
case SNTEventStateBlockLongPath: return "LONG_PATH";
|
||||
case SNTEventStateBlockUnknown: return "UNKNOWN";
|
||||
default: return "NOTRUNNING";
|
||||
@@ -177,12 +178,14 @@ static char *FormattedDateString(char *buf, size_t len) {
|
||||
}
|
||||
|
||||
std::shared_ptr<BasicString> BasicString::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTDecisionCache *decision_cache,
|
||||
bool prefix_time_name) {
|
||||
return std::make_shared<BasicString>(esapi, prefix_time_name);
|
||||
return std::make_shared<BasicString>(esapi, decision_cache, prefix_time_name);
|
||||
}
|
||||
|
||||
BasicString::BasicString(std::shared_ptr<EndpointSecurityAPI> esapi, bool prefix_time_name)
|
||||
: esapi_(esapi), prefix_time_name_(prefix_time_name) {}
|
||||
BasicString::BasicString(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTDecisionCache *decision_cache, bool prefix_time_name)
|
||||
: Serializer(std::move(decision_cache)), esapi_(esapi), prefix_time_name_(prefix_time_name) {}
|
||||
|
||||
std::string BasicString::CreateDefaultString(size_t reserved_size) {
|
||||
std::string str;
|
||||
@@ -245,9 +248,6 @@ std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg, SNTC
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString(1024); // EXECs tend to be bigger, reserve more space.
|
||||
|
||||
// Only need to grab the shared instance once
|
||||
static SNTConfigurator *configurator = [SNTConfigurator configurator];
|
||||
|
||||
str.append("action=EXEC|decision=");
|
||||
str.append(GetDecisionString(cd.decision));
|
||||
str.append("|reason=");
|
||||
@@ -291,7 +291,7 @@ std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg, SNTC
|
||||
msg.instigator().real_group());
|
||||
|
||||
str.append("|mode=");
|
||||
str.append(GetModeString([configurator clientMode]));
|
||||
str.append(GetModeString(cd.decisionClientMode));
|
||||
str.append("|path=");
|
||||
str.append(FilePath(esm.event.exec.target->executable).Sanitized());
|
||||
|
||||
|
||||
@@ -56,10 +56,10 @@ using santa::santad::logs::endpoint_security::serializers::GetModeString;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetReasonString;
|
||||
|
||||
std::string BasicStringSerializeMessage(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
es_message_t *esMsg, SNTDecisionCache *decisionCache) {
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
std::shared_ptr<Serializer> bs = BasicString::Create(mockESApi, false);
|
||||
std::shared_ptr<Serializer> bs = BasicString::Create(mockESApi, decisionCache, false);
|
||||
std::vector<uint8_t> ret = bs->SerializeMessage(Enricher().Enrich(Message(mockESApi, esMsg)));
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
@@ -69,7 +69,7 @@ std::string BasicStringSerializeMessage(std::shared_ptr<MockEndpointSecurityAPI>
|
||||
|
||||
std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
return BasicStringSerializeMessage(mockESApi, esMsg);
|
||||
return BasicStringSerializeMessage(mockESApi, esMsg, nil);
|
||||
}
|
||||
|
||||
@interface BasicStringTest : XCTestCase
|
||||
@@ -94,6 +94,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
self.testCachedDecision.sha256 = @"1234_hash";
|
||||
self.testCachedDecision.quarantineURL = @"google.com";
|
||||
self.testCachedDecision.certSHA256 = @"5678_hash";
|
||||
self.testCachedDecision.decisionClientMode = SNTClientModeLockdown;
|
||||
|
||||
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
|
||||
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
|
||||
@@ -163,7 +164,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
.WillOnce(testing::Return(es_string_token_t{5, "-l\n-t"}))
|
||||
.WillOnce(testing::Return(es_string_token_t{8, "-v\r--foo"}));
|
||||
|
||||
std::string got = BasicStringSerializeMessage(mockESApi, &esMsg);
|
||||
std::string got = BasicStringSerializeMessage(mockESApi, &esMsg, self.mockDecisionCache);
|
||||
std::string want =
|
||||
"action=EXEC|decision=ALLOW|reason=BINARY|explain=extra!|sha256=1234_hash|"
|
||||
"cert_sha256=5678_hash|cert_cn=|quarantine_url=google.com|pid=12|pidversion="
|
||||
@@ -289,7 +290,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
std::vector<uint8_t> ret =
|
||||
BasicString::Create(nullptr, false)
|
||||
BasicString::Create(nullptr, nil, false)
|
||||
->SerializeFileAccess("v1.0", "pol_name", Message(mockESApi, &esMsg),
|
||||
Enricher().Enrich(*esMsg.process), "file_target",
|
||||
FileAccessPolicyDecision::kAllowedAuditOnly);
|
||||
@@ -310,7 +311,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(mockESApi, false)
|
||||
std::vector<uint8_t> ret = BasicString::Create(mockESApi, nil, false)
|
||||
->SerializeAllowlist(Message(mockESApi, &esMsg), "test_hash");
|
||||
|
||||
XCTAssertTrue(testing::Mock::VerifyAndClearExpectations(mockESApi.get()),
|
||||
@@ -333,7 +334,8 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
se.fileBundlePath = @"file_bundle_path";
|
||||
se.filePath = @"file_path";
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeBundleHashingEvent(se);
|
||||
std::vector<uint8_t> ret =
|
||||
BasicString::Create(nullptr, nil, false)->SerializeBundleHashingEvent(se);
|
||||
std::string got(ret.begin(), ret.end());
|
||||
|
||||
std::string want = "action=BUNDLE|sha256=file_hash"
|
||||
@@ -360,7 +362,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator enableMachineIDDecoration]).andReturn(NO);
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeDiskAppeared(props);
|
||||
std::vector<uint8_t> ret = BasicString::Create(nullptr, nil, false)->SerializeDiskAppeared(props);
|
||||
std::string got(ret.begin(), ret.end());
|
||||
|
||||
std::string want = "action=DISKAPPEAR|mount=path|volume=|bsdname=bsd|fs=apfs"
|
||||
@@ -376,7 +378,8 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
@"DAMediaBSDName" : @"bsd",
|
||||
};
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeDiskDisappeared(props);
|
||||
std::vector<uint8_t> ret =
|
||||
BasicString::Create(nullptr, nil, false)->SerializeDiskDisappeared(props);
|
||||
std::string got(ret.begin(), ret.end());
|
||||
|
||||
std::string want = "action=DISKDISAPPEAR|mount=path|volume=|bsdname=bsd|machineid=my_id\n";
|
||||
@@ -418,6 +421,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
{SNTEventStateBlockCertificate, "CERT"},
|
||||
{SNTEventStateBlockScope, "SCOPE"},
|
||||
{SNTEventStateBlockTeamID, "TEAMID"},
|
||||
{SNTEventStateBlockSigningID, "SIGNINGID"},
|
||||
{SNTEventStateBlockLongPath, "LONG_PATH"},
|
||||
{SNTEventStateAllowUnknown, "UNKNOWN"},
|
||||
{SNTEventStateAllowBinary, "BINARY"},
|
||||
@@ -427,6 +431,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
{SNTEventStateAllowTransitive, "TRANSITIVE"},
|
||||
{SNTEventStateAllowPendingTransitive, "PENDING_TRANSITIVE"},
|
||||
{SNTEventStateAllowTeamID, "TEAMID"},
|
||||
{SNTEventStateAllowSigningID, "SIGNINGID"},
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToReason) {
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace santa::santad::logs::endpoint_security::serializers {
|
||||
class Empty : public Serializer {
|
||||
public:
|
||||
static std::shared_ptr<Empty> Create();
|
||||
Empty();
|
||||
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
|
||||
|
||||
@@ -31,6 +31,8 @@ std::shared_ptr<Empty> Empty::Create() {
|
||||
return std::make_shared<Empty>();
|
||||
}
|
||||
|
||||
Empty::Empty() : Serializer(nil) {}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedClose &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -25,16 +25,19 @@
|
||||
#include "Source/common/santa_proto_include_wrapper.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
class Protobuf : public Serializer {
|
||||
public:
|
||||
static std::shared_ptr<Protobuf> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi);
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
SNTDecisionCache *decision_cache);
|
||||
|
||||
Protobuf(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi);
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
SNTDecisionCache *decision_cache);
|
||||
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
|
||||
|
||||
@@ -23,13 +23,14 @@
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#include "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/common/String.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
@@ -38,6 +39,7 @@
|
||||
using google::protobuf::Arena;
|
||||
using google::protobuf::Timestamp;
|
||||
|
||||
using santa::common::NSStringToUTF8StringView;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
|
||||
@@ -63,11 +65,13 @@ namespace pbv1 = ::santa::pb::v1;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
std::shared_ptr<Protobuf> Protobuf::Create(std::shared_ptr<EndpointSecurityAPI> esapi) {
|
||||
return std::make_shared<Protobuf>(esapi);
|
||||
std::shared_ptr<Protobuf> Protobuf::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTDecisionCache *decision_cache) {
|
||||
return std::make_shared<Protobuf>(esapi, std::move(decision_cache));
|
||||
}
|
||||
|
||||
Protobuf::Protobuf(std::shared_ptr<EndpointSecurityAPI> esapi) : esapi_(esapi) {}
|
||||
Protobuf::Protobuf(std::shared_ptr<EndpointSecurityAPI> esapi, SNTDecisionCache *decision_cache)
|
||||
: Serializer(std::move(decision_cache)), esapi_(esapi) {}
|
||||
|
||||
static inline void EncodeTimestamp(Timestamp *timestamp, struct timespec ts) {
|
||||
timestamp->set_seconds(ts.tv_sec);
|
||||
@@ -94,15 +98,15 @@ static inline void EncodePath(std::string *buf, const es_file_t *es_file) {
|
||||
buf->append(std::string_view(es_file->path.data, es_file->path.length));
|
||||
}
|
||||
|
||||
static inline void EncodeString(std::string *buf, NSString *value) {
|
||||
static inline void EncodeString(std::function<std::string *()> lazy_f, NSString *value) {
|
||||
if (value) {
|
||||
buf->append(std::string_view([value UTF8String], [value length]));
|
||||
lazy_f()->append(NSStringToUTF8StringView(value));
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeString(std::string *buf, std::string_view value) {
|
||||
static inline void EncodeString(std::function<std::string *()> lazy_f, std::string_view value) {
|
||||
if (value.length() > 0) {
|
||||
buf->append(std::string_view(value.data(), value.length()));
|
||||
lazy_f()->append(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +129,7 @@ static inline void EncodeGroupInfo(::pbv1::GroupInfo *pb_group_info, gid_t gid,
|
||||
static inline void EncodeHash(::pbv1::Hash *pb_hash, NSString *sha256) {
|
||||
if (sha256) {
|
||||
pb_hash->set_type(::pbv1::Hash::HASH_ALGO_SHA256);
|
||||
pb_hash->set_hash([sha256 UTF8String], [sha256 length]);
|
||||
EncodeString([pb_hash] { return pb_hash->mutable_hash(); }, sha256);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +166,7 @@ static inline void EncodeFileInfo(::pbv1::FileInfo *pb_file, const es_file_t *es
|
||||
|
||||
static inline void EncodeFileInfoLight(::pbv1::FileInfoLight *pb_file, std::string_view path,
|
||||
bool truncated) {
|
||||
EncodeString(pb_file->mutable_path(), path);
|
||||
EncodeString([pb_file] { return pb_file->mutable_path(); }, path);
|
||||
pb_file->set_truncated(truncated);
|
||||
}
|
||||
|
||||
@@ -262,9 +266,7 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
EncodeHash(pb_cert_info->mutable_hash(), cert_hash);
|
||||
}
|
||||
|
||||
if (common_name) {
|
||||
pb_cert_info->set_common_name([common_name UTF8String], [common_name length]);
|
||||
}
|
||||
EncodeString([pb_cert_info] { return pb_cert_info->mutable_common_name(); }, common_name);
|
||||
}
|
||||
|
||||
::pbv1::Execution::Decision GetDecisionEnum(SNTEventState event_state) {
|
||||
@@ -286,11 +288,13 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
case SNTEventStateAllowCertificate: return ::pbv1::Execution::REASON_CERT;
|
||||
case SNTEventStateAllowScope: return ::pbv1::Execution::REASON_SCOPE;
|
||||
case SNTEventStateAllowTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
|
||||
case SNTEventStateAllowSigningID: return ::pbv1::Execution::REASON_SIGNING_ID;
|
||||
case SNTEventStateAllowUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
|
||||
case SNTEventStateBlockBinary: return ::pbv1::Execution::REASON_BINARY;
|
||||
case SNTEventStateBlockCertificate: return ::pbv1::Execution::REASON_CERT;
|
||||
case SNTEventStateBlockScope: return ::pbv1::Execution::REASON_SCOPE;
|
||||
case SNTEventStateBlockTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
|
||||
case SNTEventStateBlockSigningID: return ::pbv1::Execution::REASON_SIGNING_ID;
|
||||
case SNTEventStateBlockLongPath: return ::pbv1::Execution::REASON_LONG_PATH;
|
||||
case SNTEventStateBlockUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
|
||||
default: return ::pbv1::Execution::REASON_NOT_RUNNING;
|
||||
@@ -356,7 +360,7 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
::pbv1::SantaMessage *santa_msg = Arena::CreateMessage<::pbv1::SantaMessage>(arena);
|
||||
|
||||
if (EnabledMachineID()) {
|
||||
EncodeString(santa_msg->mutable_machine_id(), MachineID());
|
||||
EncodeString([santa_msg] { return santa_msg->mutable_machine_id(); }, MachineID());
|
||||
}
|
||||
EncodeTimestamp(santa_msg->mutable_event_time(), event_time);
|
||||
EncodeTimestamp(santa_msg->mutable_processed_time(), processed_time);
|
||||
@@ -422,9 +426,6 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCach
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
// Only need to grab the shared instance once
|
||||
static SNTConfigurator *configurator = [SNTConfigurator configurator];
|
||||
|
||||
GetDecisionEnum(cd.decision);
|
||||
|
||||
::pbv1::Execution *pb_exec = santa_msg->mutable_execution();
|
||||
@@ -485,24 +486,17 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCach
|
||||
|
||||
pb_exec->set_decision(GetDecisionEnum(cd.decision));
|
||||
pb_exec->set_reason(GetReasonEnum(cd.decision));
|
||||
pb_exec->set_mode(GetModeEnum([configurator clientMode]));
|
||||
pb_exec->set_mode(GetModeEnum(cd.decisionClientMode));
|
||||
|
||||
if (cd.certSHA256 || cd.certCommonName) {
|
||||
EncodeCertificateInfo(pb_exec->mutable_certificate_info(), cd.certSHA256, cd.certCommonName);
|
||||
}
|
||||
|
||||
if (cd.decisionExtra) {
|
||||
pb_exec->set_explain([cd.decisionExtra UTF8String], [cd.decisionExtra length]);
|
||||
}
|
||||
|
||||
if (cd.quarantineURL) {
|
||||
pb_exec->set_quarantine_url([cd.quarantineURL UTF8String], [cd.quarantineURL length]);
|
||||
}
|
||||
EncodeString([pb_exec] { return pb_exec->mutable_explain(); }, cd.decisionExtra);
|
||||
EncodeString([pb_exec] { return pb_exec->mutable_quarantine_url(); }, cd.quarantineURL);
|
||||
|
||||
NSString *orig_path = Utilities::OriginalPathForTranslocation(msg.es_msg().event.exec.target);
|
||||
if (orig_path) {
|
||||
pb_exec->set_original_path([orig_path UTF8String], [orig_path length]);
|
||||
}
|
||||
EncodeString([pb_exec] { return pb_exec->mutable_original_path(); }, orig_path);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
@@ -594,8 +588,9 @@ std::vector<uint8_t> Protobuf::SerializeFileAccess(const std::string &policy_ver
|
||||
EncodeProcessInfo(file_access->mutable_instigator(), msg->version, msg->process,
|
||||
enriched_process);
|
||||
EncodeFileInfoLight(file_access->mutable_target(), target, false);
|
||||
EncodeString(file_access->mutable_policy_version(), policy_version);
|
||||
EncodeString(file_access->mutable_policy_name(), policy_name);
|
||||
EncodeString([file_access] { return file_access->mutable_policy_version(); }, policy_version);
|
||||
EncodeString([file_access] { return file_access->mutable_policy_name(); }, policy_name);
|
||||
|
||||
file_access->set_access_type(GetAccessType(msg->event_type));
|
||||
file_access->set_policy_decision(GetPolicyDecision(decision));
|
||||
|
||||
@@ -629,10 +624,12 @@ std::vector<uint8_t> Protobuf::SerializeBundleHashingEvent(SNTStoredEvent *event
|
||||
|
||||
EncodeHash(pb_bundle->mutable_file_hash(), event.fileSHA256);
|
||||
EncodeHash(pb_bundle->mutable_bundle_hash(), event.fileBundleHash);
|
||||
pb_bundle->set_bundle_name([NonNull(event.fileBundleName) UTF8String]);
|
||||
pb_bundle->set_bundle_id([NonNull(event.fileBundleID) UTF8String]);
|
||||
pb_bundle->set_bundle_path([NonNull(event.fileBundlePath) UTF8String]);
|
||||
pb_bundle->set_path([NonNull(event.filePath) UTF8String]);
|
||||
EncodeString([pb_bundle] { return pb_bundle->mutable_bundle_name(); },
|
||||
NonNull(event.fileBundleName));
|
||||
EncodeString([pb_bundle] { return pb_bundle->mutable_bundle_id(); }, NonNull(event.fileBundleID));
|
||||
EncodeString([pb_bundle] { return pb_bundle->mutable_bundle_path(); },
|
||||
NonNull(event.fileBundlePath));
|
||||
EncodeString([pb_bundle] { return pb_bundle->mutable_path(); }, NonNull(event.filePath));
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
@@ -652,14 +649,14 @@ static void EncodeDisk(::pbv1::Disk *pb_disk, ::pbv1::Disk_Action action, NSDict
|
||||
stringWithFormat:@"%@ %@", NonNull(props[@"DADeviceVendor"]), NonNull(props[@"DADeviceModel"])];
|
||||
model = [model stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
|
||||
EncodeString(pb_disk->mutable_mount(), [props[@"DAVolumePath"] path]);
|
||||
EncodeString(pb_disk->mutable_volume(), props[@"DAVolumeName"]);
|
||||
EncodeString(pb_disk->mutable_bsd_name(), props[@"DAMediaBSDName"]);
|
||||
EncodeString(pb_disk->mutable_fs(), props[@"DAVolumeKind"]);
|
||||
EncodeString(pb_disk->mutable_model(), model);
|
||||
EncodeString(pb_disk->mutable_serial(), serial);
|
||||
EncodeString(pb_disk->mutable_bus(), props[@"DADeviceProtocol"]);
|
||||
EncodeString(pb_disk->mutable_dmg_path(), dmg_path);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_mount(); }, [props[@"DAVolumePath"] path]);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_volume(); }, props[@"DAVolumeName"]);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_bsd_name(); }, props[@"DAMediaBSDName"]);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_fs(); }, props[@"DAVolumeKind"]);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_model(); }, model);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_serial(); }, serial);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_bus(); }, props[@"DADeviceProtocol"]);
|
||||
EncodeString([pb_disk] { return pb_disk->mutable_dmg_path(); }, dmg_path);
|
||||
|
||||
if (props[@"DAAppearanceTime"]) {
|
||||
// Note: `DAAppearanceTime` is set via `CFAbsoluteTimeGetCurrent`, which uses the defined
|
||||
|
||||
@@ -128,12 +128,6 @@ bool CompareTime(const Timestamp ×tamp, struct timespec ts) {
|
||||
return timestamp.seconds() == ts.tv_sec && timestamp.nanos() == ts.tv_nsec;
|
||||
}
|
||||
|
||||
void CheckSantaMessage(const ::pbv1::SantaMessage &santaMsg, const es_message_t &esMsg,
|
||||
struct timespec enrichmentTime) {
|
||||
XCTAssertTrue(CompareTime(santaMsg.processed_time(), enrichmentTime));
|
||||
XCTAssertTrue(CompareTime(santaMsg.event_time(), esMsg.time));
|
||||
}
|
||||
|
||||
const google::protobuf::Message &SantaMessageEvent(const ::pbv1::SantaMessage &santaMsg) {
|
||||
switch (santaMsg.event_case()) {
|
||||
case ::pbv1::SantaMessage::kExecution: return santaMsg.execution();
|
||||
@@ -167,23 +161,10 @@ std::string ConvertMessageToJsonString(const ::pbv1::SantaMessage &santaMsg) {
|
||||
return json;
|
||||
}
|
||||
|
||||
void CheckProto(const ::pbv1::SantaMessage &santaMsg,
|
||||
std::shared_ptr<EnrichedMessage> enrichedMsg) {
|
||||
return std::visit(
|
||||
[santaMsg](const EnrichedEventType &enrichedEvent) {
|
||||
CheckSantaMessage(santaMsg, enrichedEvent.es_msg(), enrichedEvent.enrichment_time());
|
||||
NSString *wantData = LoadTestJson(EventTypeToFilename(enrichedEvent.es_msg().event_type),
|
||||
enrichedEvent.es_msg().version);
|
||||
std::string got = ConvertMessageToJsonString(santaMsg);
|
||||
|
||||
XCTAssertEqualObjects([NSString stringWithUTF8String:got.c_str()], wantData);
|
||||
},
|
||||
enrichedMsg->GetEnrichedMessage());
|
||||
}
|
||||
|
||||
void SerializeAndCheck(es_event_type_t eventType,
|
||||
void (^messageSetup)(std::shared_ptr<MockEndpointSecurityAPI>,
|
||||
es_message_t *)) {
|
||||
es_message_t *),
|
||||
SNTDecisionCache *decisionCache) {
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
for (uint32_t cur_version = 1; cur_version <= MaxSupportedESMessageVersionForCurrentOS();
|
||||
@@ -204,16 +185,33 @@ void SerializeAndCheck(es_event_type_t eventType,
|
||||
|
||||
messageSetup(mockESApi, &esMsg);
|
||||
|
||||
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi);
|
||||
std::shared_ptr<EnrichedMessage> enrichedMsg = Enricher().Enrich(Message(mockESApi, &esMsg));
|
||||
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi, decisionCache);
|
||||
std::unique_ptr<EnrichedMessage> enrichedMsg = Enricher().Enrich(Message(mockESApi, &esMsg));
|
||||
|
||||
std::vector<uint8_t> vec = bs->SerializeMessage(enrichedMsg);
|
||||
// Copy some values we need to check later before the object is moved out of this funciton
|
||||
struct timespec enrichmentTime;
|
||||
struct timespec msgTime;
|
||||
NSString *wantData = std::visit(
|
||||
[&msgTime, &enrichmentTime](const EnrichedEventType &enrichedEvent) {
|
||||
msgTime = enrichedEvent.es_msg().time;
|
||||
enrichmentTime = enrichedEvent.enrichment_time();
|
||||
|
||||
return LoadTestJson(EventTypeToFilename(enrichedEvent.es_msg().event_type),
|
||||
enrichedEvent.es_msg().version);
|
||||
},
|
||||
enrichedMsg->GetEnrichedMessage());
|
||||
|
||||
std::vector<uint8_t> vec = bs->SerializeMessage(std::move(enrichedMsg));
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
|
||||
CheckProto(santaMsg, enrichedMsg);
|
||||
std::string gotData = ConvertMessageToJsonString(santaMsg);
|
||||
|
||||
XCTAssertTrue(CompareTime(santaMsg.processed_time(), enrichmentTime));
|
||||
XCTAssertTrue(CompareTime(santaMsg.event_time(), msgTime));
|
||||
XCTAssertEqualObjects([NSString stringWithUTF8String:gotData.c_str()], wantData);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
@@ -226,7 +224,7 @@ void SerializeAndCheckNonESEvents(
|
||||
const Message &msg)) {
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi);
|
||||
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi, nil);
|
||||
|
||||
for (uint32_t cur_version = 1; cur_version <= MaxSupportedESMessageVersionForCurrentOS();
|
||||
cur_version++) {
|
||||
@@ -280,6 +278,7 @@ void SerializeAndCheckNonESEvents(
|
||||
self.testCachedDecision.sha256 = @"1234_file_hash";
|
||||
self.testCachedDecision.quarantineURL = @"google.com";
|
||||
self.testCachedDecision.certSHA256 = @"5678_cert_hash";
|
||||
self.testCachedDecision.decisionClientMode = SNTClientModeLockdown;
|
||||
|
||||
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
|
||||
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
|
||||
@@ -293,25 +292,33 @@ void SerializeAndCheckNonESEvents(
|
||||
[self.mockDecisionCache stopMocking];
|
||||
}
|
||||
|
||||
- (void)serializeAndCheckEvent:(es_event_type_t)eventType
|
||||
messageSetup:(void (^)(std::shared_ptr<MockEndpointSecurityAPI>,
|
||||
es_message_t *))messageSetup {
|
||||
SerializeAndCheck(eventType, messageSetup, self.mockDecisionCache);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageClose {
|
||||
__block es_file_t file = MakeESFile("close_file", MakeStat(300));
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_CLOSE,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.close.modified = true;
|
||||
esMsg->event.close.target = &file;
|
||||
});
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_CLOSE
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.close.modified = true;
|
||||
esMsg->event.close.target = &file;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExchange {
|
||||
__block es_file_t file1 = MakeESFile("exchange_file_1", MakeStat(300));
|
||||
__block es_file_t file2 = MakeESFile("exchange_file_1", MakeStat(400));
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.exchangedata.file1 = &file1;
|
||||
esMsg->event.exchangedata.file2 = &file2;
|
||||
});
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.exchangedata.file1 = &file1;
|
||||
esMsg->event.exchangedata.file2 = &file2;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testGetDecisionEnum {
|
||||
@@ -335,7 +342,7 @@ void SerializeAndCheckNonESEvents(
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToDecision) {
|
||||
XCTAssertEqual(GetDecisionEnum(kv.first), kv.second, @"Bad decision for state: %ld", kv.first);
|
||||
XCTAssertEqual(GetDecisionEnum(kv.first), kv.second, @"Bad decision for state: %llu", kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,6 +355,7 @@ void SerializeAndCheckNonESEvents(
|
||||
{SNTEventStateBlockCertificate, ::pbv1::Execution::REASON_CERT},
|
||||
{SNTEventStateBlockScope, ::pbv1::Execution::REASON_SCOPE},
|
||||
{SNTEventStateBlockTeamID, ::pbv1::Execution::REASON_TEAM_ID},
|
||||
{SNTEventStateBlockSigningID, ::pbv1::Execution::REASON_SIGNING_ID},
|
||||
{SNTEventStateBlockLongPath, ::pbv1::Execution::REASON_LONG_PATH},
|
||||
{SNTEventStateAllowUnknown, ::pbv1::Execution::REASON_UNKNOWN},
|
||||
{SNTEventStateAllowBinary, ::pbv1::Execution::REASON_BINARY},
|
||||
@@ -357,10 +365,11 @@ void SerializeAndCheckNonESEvents(
|
||||
{SNTEventStateAllowTransitive, ::pbv1::Execution::REASON_TRANSITIVE},
|
||||
{SNTEventStateAllowPendingTransitive, ::pbv1::Execution::REASON_PENDING_TRANSITIVE},
|
||||
{SNTEventStateAllowTeamID, ::pbv1::Execution::REASON_TEAM_ID},
|
||||
{SNTEventStateAllowSigningID, ::pbv1::Execution::REASON_SIGNING_ID},
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToReason) {
|
||||
XCTAssertEqual(GetReasonEnum(kv.first), kv.second, @"Bad reason for state: %ld", kv.first);
|
||||
XCTAssertEqual(GetReasonEnum(kv.first), kv.second, @"Bad reason for state: %llu", kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,7 +382,7 @@ void SerializeAndCheckNonESEvents(
|
||||
};
|
||||
|
||||
for (const auto &kv : clientModeToExecMode) {
|
||||
XCTAssertEqual(GetModeEnum(kv.first), kv.second, @"Bad mode for state: %ld", kv.first);
|
||||
XCTAssertEqual(GetModeEnum(kv.first), kv.second, @"Bad mode for client mode: %ld", kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,45 +422,48 @@ void SerializeAndCheckNonESEvents(
|
||||
procTarget.signing_id = MakeESStringToken("my_signing_id");
|
||||
procTarget.team_id = MakeESStringToken("my_team_id");
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXEC, ^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.exec.target = &procTarget;
|
||||
esMsg->event.exec.cwd = &fileCwd;
|
||||
esMsg->event.exec.script = &fileScript;
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_EXEC
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.exec.target = &procTarget;
|
||||
esMsg->event.exec.cwd = &fileCwd;
|
||||
esMsg->event.exec.script = &fileScript;
|
||||
|
||||
// For version 5, simulate a "truncated" set of FDs
|
||||
if (esMsg->version == 5) {
|
||||
esMsg->event.exec.last_fd = 123;
|
||||
} else {
|
||||
esMsg->event.exec.last_fd = 3;
|
||||
}
|
||||
// For version 5, simulate a "truncated" set of FDs
|
||||
if (esMsg->version == 5) {
|
||||
esMsg->event.exec.last_fd = 123;
|
||||
} else {
|
||||
esMsg->event.exec.last_fd = 3;
|
||||
}
|
||||
|
||||
EXPECT_CALL(*mockESApi, ExecArgCount).WillOnce(testing::Return(3));
|
||||
EXPECT_CALL(*mockESApi, ExecArg)
|
||||
.WillOnce(testing::Return(MakeESStringToken("exec_path")))
|
||||
.WillOnce(testing::Return(MakeESStringToken("-l")))
|
||||
.WillOnce(testing::Return(MakeESStringToken("--foo")));
|
||||
EXPECT_CALL(*mockESApi, ExecArgCount).WillOnce(testing::Return(3));
|
||||
EXPECT_CALL(*mockESApi, ExecArg)
|
||||
.WillOnce(testing::Return(MakeESStringToken("exec_path")))
|
||||
.WillOnce(testing::Return(MakeESStringToken("-l")))
|
||||
.WillOnce(testing::Return(MakeESStringToken("--foo")));
|
||||
|
||||
EXPECT_CALL(*mockESApi, ExecEnvCount).WillOnce(testing::Return(2));
|
||||
EXPECT_CALL(*mockESApi, ExecEnv)
|
||||
.WillOnce(testing::Return(MakeESStringToken("ENV_PATH=/path/to/bin:/and/another")))
|
||||
.WillOnce(testing::Return(MakeESStringToken("DEBUG=1")));
|
||||
EXPECT_CALL(*mockESApi, ExecEnvCount).WillOnce(testing::Return(2));
|
||||
EXPECT_CALL(*mockESApi, ExecEnv)
|
||||
.WillOnce(
|
||||
testing::Return(MakeESStringToken("ENV_PATH=/path/to/bin:/and/another")))
|
||||
.WillOnce(testing::Return(MakeESStringToken("DEBUG=1")));
|
||||
|
||||
if (esMsg->version >= 4) {
|
||||
EXPECT_CALL(*mockESApi, ExecFDCount).WillOnce(testing::Return(3));
|
||||
EXPECT_CALL(*mockESApi, ExecFD)
|
||||
.WillOnce(testing::Return(&fd1))
|
||||
.WillOnce(testing::Return(&fd2))
|
||||
.WillOnce(testing::Return(&fd3));
|
||||
}
|
||||
});
|
||||
if (esMsg->version >= 4) {
|
||||
EXPECT_CALL(*mockESApi, ExecFDCount).WillOnce(testing::Return(3));
|
||||
EXPECT_CALL(*mockESApi, ExecFD)
|
||||
.WillOnce(testing::Return(&fd1))
|
||||
.WillOnce(testing::Return(&fd2))
|
||||
.WillOnce(testing::Return(&fd3));
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExit {
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXIT,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.exit.stat = W_EXITCODE(1, 0);
|
||||
});
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_EXIT
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.exit.stat = W_EXITCODE(1, 0);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testEncodeExitStatus {
|
||||
@@ -484,10 +496,11 @@ void SerializeAndCheckNonESEvents(
|
||||
MakeESProcess(&procFileChild, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
procChild.tty = &ttyFileChild;
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_FORK,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.fork.child = &procChild;
|
||||
});
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_FORK
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.fork.child = &procChild;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageLink {
|
||||
@@ -495,12 +508,13 @@ void SerializeAndCheckNonESEvents(
|
||||
__block es_file_t fileTargetDir = MakeESFile("target_dir");
|
||||
es_string_token_t targetTok = MakeESStringToken("target_file");
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_LINK,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.link.source = &fileSource;
|
||||
esMsg->event.link.target_dir = &fileTargetDir;
|
||||
esMsg->event.link.target_filename = targetTok;
|
||||
});
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_LINK
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.link.source = &fileSource;
|
||||
esMsg->event.link.target_dir = &fileTargetDir;
|
||||
esMsg->event.link.target_filename = targetTok;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageRename {
|
||||
@@ -508,30 +522,32 @@ void SerializeAndCheckNonESEvents(
|
||||
__block es_file_t fileTargetDir = MakeESFile("target_dir");
|
||||
es_string_token_t targetTok = MakeESStringToken("target_file");
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_RENAME,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.rename.source = &fileSource;
|
||||
// Test new and existing destination types
|
||||
if (esMsg->version == 4) {
|
||||
esMsg->event.rename.destination.existing_file = &fileTargetDir;
|
||||
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
|
||||
} else {
|
||||
esMsg->event.rename.destination.new_path.dir = &fileTargetDir;
|
||||
esMsg->event.rename.destination.new_path.filename = targetTok;
|
||||
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
|
||||
}
|
||||
});
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_RENAME
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.rename.source = &fileSource;
|
||||
// Test new and existing destination types
|
||||
if (esMsg->version == 4) {
|
||||
esMsg->event.rename.destination.existing_file = &fileTargetDir;
|
||||
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
|
||||
} else {
|
||||
esMsg->event.rename.destination.new_path.dir = &fileTargetDir;
|
||||
esMsg->event.rename.destination.new_path.filename = targetTok;
|
||||
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageUnlink {
|
||||
__block es_file_t fileTarget = MakeESFile("unlink_file", MakeStat(300));
|
||||
__block es_file_t fileTargetParent = MakeESFile("unlink_file_parent", MakeStat(400));
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_UNLINK,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.unlink.target = &fileTarget;
|
||||
esMsg->event.unlink.parent_dir = &fileTargetParent;
|
||||
});
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_UNLINK
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.unlink.target = &fileTarget;
|
||||
esMsg->event.unlink.parent_dir = &fileTargetParent;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testGetAccessType {
|
||||
@@ -608,7 +624,7 @@ void SerializeAndCheckNonESEvents(
|
||||
se.fileBundlePath = @"file_bundle_path";
|
||||
se.filePath = @"file_path";
|
||||
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeBundleHashingEvent(se);
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr, nil)->SerializeBundleHashingEvent(se);
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
@@ -643,7 +659,7 @@ void SerializeAndCheckNonESEvents(
|
||||
@"DADeviceProtocol" : @"usb",
|
||||
};
|
||||
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeDiskAppeared(props);
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr, nil)->SerializeDiskAppeared(props);
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
@@ -680,7 +696,7 @@ void SerializeAndCheckNonESEvents(
|
||||
@"DADeviceProtocol" : @"usb",
|
||||
};
|
||||
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeDiskDisappeared(props);
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr, nil)->SerializeDiskDisappeared(props);
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
|
||||
@@ -51,8 +51,7 @@ class SanitizableString {
|
||||
friend std::ostream &operator<<(std::ostream &ss, const SanitizableString &sani_string);
|
||||
|
||||
private:
|
||||
const char *data_;
|
||||
size_t length_;
|
||||
std::string_view data_;
|
||||
mutable bool sanitized_ = false;
|
||||
mutable std::optional<std::string> sanitized_string_;
|
||||
};
|
||||
|
||||
@@ -14,37 +14,35 @@
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h"
|
||||
|
||||
#include "Source/common/String.h"
|
||||
|
||||
using santa::common::NSStringToUTF8StringView;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
SanitizableString::SanitizableString(const es_file_t *file)
|
||||
: data_(file->path.data), length_(file->path.length) {}
|
||||
: data_(file->path.data, file->path.length) {}
|
||||
|
||||
SanitizableString::SanitizableString(const es_string_token_t &tok)
|
||||
: data_(tok.data), length_(tok.length) {}
|
||||
SanitizableString::SanitizableString(const es_string_token_t &tok) : data_(tok.data, tok.length) {}
|
||||
|
||||
SanitizableString::SanitizableString(NSString *str)
|
||||
: data_([str UTF8String]), length_([str length]) {}
|
||||
SanitizableString::SanitizableString(NSString *str) : data_(NSStringToUTF8StringView(str)) {}
|
||||
|
||||
SanitizableString::SanitizableString(const char *str, size_t len) : data_(str), length_(len) {}
|
||||
SanitizableString::SanitizableString(const char *str, size_t len) : data_(str, len) {}
|
||||
|
||||
std::string_view SanitizableString::String() const {
|
||||
return std::string_view(data_, length_);
|
||||
return data_;
|
||||
}
|
||||
|
||||
std::string_view SanitizableString::Sanitized() const {
|
||||
if (!sanitized_) {
|
||||
sanitized_ = true;
|
||||
sanitized_string_ = SanitizeString(data_, length_);
|
||||
sanitized_string_ = SanitizeString(data_.data(), data_.length());
|
||||
}
|
||||
|
||||
if (sanitized_string_.has_value()) {
|
||||
return sanitized_string_.value();
|
||||
} else {
|
||||
if (data_) {
|
||||
return std::string_view(data_, length_);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
return data_;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,14 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
@class SNTStoredEvent;
|
||||
|
||||
@@ -30,11 +32,11 @@ namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
class Serializer {
|
||||
public:
|
||||
Serializer();
|
||||
Serializer(SNTDecisionCache *decision_cache);
|
||||
virtual ~Serializer() = default;
|
||||
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage> msg) {
|
||||
std::unique_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage> msg) {
|
||||
return std::visit([this](const auto &arg) { return this->SerializeMessageTemplate(arg); },
|
||||
msg->GetEnrichedMessage());
|
||||
}
|
||||
@@ -96,6 +98,7 @@ class Serializer {
|
||||
|
||||
bool enabled_machine_id_ = false;
|
||||
std::string machine_id_;
|
||||
SNTDecisionCache *decision_cache_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace es = santa::santad::event_providers::endpoint_security;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
Serializer::Serializer() {
|
||||
Serializer::Serializer(SNTDecisionCache *decision_cache) : decision_cache_(decision_cache) {
|
||||
if ([[SNTConfigurator configurator] enableMachineIDDecoration]) {
|
||||
enabled_machine_id_ = true;
|
||||
machine_id_ = [[[SNTConfigurator configurator] machineID] UTF8String] ?: "";
|
||||
@@ -46,17 +46,15 @@ std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedExch
|
||||
return SerializeMessage(msg);
|
||||
}
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedExec &msg) {
|
||||
static SNTDecisionCache *decision_cache = [SNTDecisionCache sharedCache];
|
||||
|
||||
SNTCachedDecision *cd;
|
||||
const es_message_t &es_msg = msg.es_msg();
|
||||
if (es_msg.action_type == ES_ACTION_TYPE_NOTIFY &&
|
||||
es_msg.action.notify.result.auth == ES_AUTH_RESULT_ALLOW) {
|
||||
// For allowed execs, cached decision timestamps must be updated
|
||||
cd = [decision_cache
|
||||
cd = [decision_cache_
|
||||
resetTimestampForCachedDecision:msg.es_msg().event.exec.target->executable->stat];
|
||||
} else {
|
||||
cd = [decision_cache cachedDecisionForFile:msg.es_msg().event.exec.target->executable->stat];
|
||||
cd = [decision_cache_ cachedDecisionForFile:msg.es_msg().event.exec.target->executable->stat];
|
||||
}
|
||||
|
||||
return SerializeMessage(msg, cd);
|
||||
|
||||
@@ -144,7 +144,10 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
// Check if there is an existing (non-transitive) rule for this file. We leave existing rules
|
||||
// alone, so that a allowlist or blocklist rule can't be overwritten by a transitive one.
|
||||
SNTRuleTable *ruleTable = [SNTDatabaseController ruleTable];
|
||||
SNTRule *prevRule = [ruleTable ruleForBinarySHA256:fi.SHA256 certificateSHA256:nil teamID:nil];
|
||||
SNTRule *prevRule = [ruleTable ruleForBinarySHA256:fi.SHA256
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
if (!prevRule || prevRule.state == SNTRuleStateAllowTransitive) {
|
||||
// Construct a new transitive allowlist rule for the executable.
|
||||
SNTRule *rule = [[SNTRule alloc] initWithIdentifier:fi.SHA256
|
||||
|
||||
@@ -39,6 +39,7 @@ using santa::santad::data_layer::WatchItems;
|
||||
using santa::santad::data_layer::WatchItemsState;
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::FlushCacheReason;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
|
||||
// Globals used by the santad watchdog thread
|
||||
@@ -84,12 +85,9 @@ double watchdogRAMPeak = 0;
|
||||
reply([counts[0] unsignedLongLongValue], [counts[1] unsignedLongLongValue]);
|
||||
}
|
||||
|
||||
- (void)flushAllCaches {
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
}
|
||||
|
||||
- (void)flushCache:(void (^)(BOOL))reply {
|
||||
[self flushAllCaches];
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kAllCaches,
|
||||
FlushCacheReason::kExplicitCommand);
|
||||
reply(YES);
|
||||
}
|
||||
|
||||
@@ -100,10 +98,10 @@ double watchdogRAMPeak = 0;
|
||||
#pragma mark Database ops
|
||||
|
||||
- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive, int64_t teamID))reply {
|
||||
int64_t transitive, int64_t teamID, int64_t signingID))reply {
|
||||
SNTRuleTable *rdb = [SNTDatabaseController ruleTable];
|
||||
reply([rdb binaryRuleCount], [rdb certificateRuleCount], [rdb compilerRuleCount],
|
||||
[rdb transitiveRuleCount], [rdb teamIDRuleCount]);
|
||||
[rdb transitiveRuleCount], [rdb teamIDRuleCount], [rdb signingIDRuleCount]);
|
||||
}
|
||||
|
||||
- (void)databaseRuleAddRules:(NSArray *)rules
|
||||
@@ -125,7 +123,7 @@ double watchdogRAMPeak = 0;
|
||||
// The actual cache flushing happens after the new rules have been added to the database.
|
||||
if (flushCache) {
|
||||
LOGI(@"Flushing caches");
|
||||
[self flushAllCaches];
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kAllCaches, FlushCacheReason::kRulesChanged);
|
||||
}
|
||||
|
||||
reply(error);
|
||||
@@ -146,8 +144,10 @@ double watchdogRAMPeak = 0;
|
||||
- (void)databaseRuleForBinarySHA256:(NSString *)binarySHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
reply:(void (^)(SNTRule *))reply {
|
||||
reply([[SNTDatabaseController ruleTable] ruleForBinarySHA256:binarySHA256
|
||||
signingID:signingID
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID]);
|
||||
}
|
||||
@@ -162,11 +162,13 @@ double watchdogRAMPeak = 0;
|
||||
fileSHA256:(NSString *)fileSHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
reply:(void (^)(SNTEventState))reply {
|
||||
reply([self.policyProcessor decisionForFilePath:filePath
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID]
|
||||
teamID:teamID
|
||||
signingID:signingID]
|
||||
.decision);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,6 @@ static NSString *const kEventsDatabaseName = @"events.db";
|
||||
NSString *fullPath =
|
||||
[[SNTDatabaseController databasePath] stringByAppendingPathComponent:kEventsDatabaseName];
|
||||
FMDatabaseQueue *dbq = [[FMDatabaseQueue alloc] initWithPath:fullPath];
|
||||
chown([fullPath UTF8String], 0, 0);
|
||||
chmod([fullPath UTF8String], 0600);
|
||||
|
||||
#ifndef DEBUG
|
||||
[dbq inDatabase:^(FMDatabase *db) {
|
||||
@@ -49,6 +47,9 @@ static NSString *const kEventsDatabaseName = @"events.db";
|
||||
#endif
|
||||
|
||||
eventDatabase = [[SNTEventTable alloc] initWithDatabaseQueue:dbq];
|
||||
|
||||
chown([fullPath UTF8String], 0, 0);
|
||||
chmod([fullPath UTF8String], 0600);
|
||||
});
|
||||
|
||||
return eventDatabase;
|
||||
@@ -62,8 +63,6 @@ static NSString *const kEventsDatabaseName = @"events.db";
|
||||
NSString *fullPath =
|
||||
[[SNTDatabaseController databasePath] stringByAppendingPathComponent:kRulesDatabaseName];
|
||||
FMDatabaseQueue *dbq = [[FMDatabaseQueue alloc] initWithPath:fullPath];
|
||||
chown([fullPath UTF8String], 0, 0);
|
||||
chmod([fullPath UTF8String], 0600);
|
||||
|
||||
#ifndef DEBUG
|
||||
[dbq inDatabase:^(FMDatabase *db) {
|
||||
@@ -72,6 +71,9 @@ static NSString *const kEventsDatabaseName = @"events.db";
|
||||
#endif
|
||||
|
||||
ruleDatabase = [[SNTRuleTable alloc] initWithDatabaseQueue:dbq];
|
||||
|
||||
chown([fullPath UTF8String], 0, 0);
|
||||
chmod([fullPath UTF8String], 0600);
|
||||
});
|
||||
return ruleDatabase;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ const static NSString *kBlockCertificate = @"BlockCertificate";
|
||||
const static NSString *kAllowCertificate = @"AllowCertificate";
|
||||
const static NSString *kBlockTeamID = @"BlockTeamID";
|
||||
const static NSString *kAllowTeamID = @"AllowTeamID";
|
||||
const static NSString *kBlockSigningID = @"BlockSigningID";
|
||||
const static NSString *kAllowSigningID = @"AllowSigningID";
|
||||
const static NSString *kBlockScope = @"BlockScope";
|
||||
const static NSString *kAllowScope = @"AllowScope";
|
||||
const static NSString *kAllowUnknown = @"AllowUnknown";
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#include "Source/common/SantaVnode.h"
|
||||
#include "Source/common/String.h"
|
||||
#import "Source/santad/DataLayer/SNTEventTable.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
@@ -107,6 +108,8 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
case SNTEventStateAllowCertificate: eventTypeStr = kAllowCertificate; break;
|
||||
case SNTEventStateBlockTeamID: eventTypeStr = kBlockTeamID; break;
|
||||
case SNTEventStateAllowTeamID: eventTypeStr = kAllowTeamID; break;
|
||||
case SNTEventStateBlockSigningID: eventTypeStr = kBlockSigningID; break;
|
||||
case SNTEventStateAllowSigningID: eventTypeStr = kAllowSigningID; break;
|
||||
case SNTEventStateBlockScope: eventTypeStr = kBlockScope; break;
|
||||
case SNTEventStateAllowScope: eventTypeStr = kAllowScope; break;
|
||||
case SNTEventStateBlockUnknown: eventTypeStr = kBlockUnknown; break;
|
||||
@@ -199,7 +202,8 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
// TODO(markowsky): Maybe add a metric here for how many large executables we're seeing.
|
||||
// if (binInfo.fileSize > SomeUpperLimit) ...
|
||||
|
||||
SNTCachedDecision *cd = [self.policyProcessor decisionForFileInfo:binInfo];
|
||||
SNTCachedDecision *cd = [self.policyProcessor decisionForFileInfo:binInfo
|
||||
targetProcess:targetProc];
|
||||
|
||||
cd.vnodeId = SantaVnode::VnodeForFile(targetProc->executable);
|
||||
|
||||
@@ -236,6 +240,7 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
|
||||
se.signingChain = cd.certChain;
|
||||
se.teamID = cd.teamID;
|
||||
se.signingID = cd.signingID;
|
||||
se.pid = @(audit_token_to_pid(targetProc->audit_token));
|
||||
se.ppid = @(audit_token_to_pid(targetProc->parent_audit_token));
|
||||
se.parentName = @(esMsg.ParentProcessName().c_str());
|
||||
@@ -293,7 +298,7 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
NSAttributedString *s = [SNTBlockMessage attributedBlockMessageForEvent:se
|
||||
customMessage:cd.customMsg];
|
||||
|
||||
if (targetProc->tty && targetProc->tty->path.length > 0) {
|
||||
if (targetProc->tty && targetProc->tty->path.length > 0 && !config.enableSilentTTYMode) {
|
||||
NSMutableString *msg = [NSMutableString stringWithCapacity:1024];
|
||||
[msg appendFormat:@"\n\033[1mSanta\033[0m\n\n%@\n\n", s.string];
|
||||
[msg appendFormat:@"\033[1mPath: \033[0m %@\n"
|
||||
@@ -360,7 +365,8 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
|
||||
- (void)printMessage:(NSString *)msg toTTY:(const char *)path {
|
||||
int fd = open(path, O_WRONLY | O_NOCTTY);
|
||||
write(fd, msg.UTF8String, msg.length);
|
||||
std::string_view str = santa::common::NSStringToUTF8StringView(msg);
|
||||
write(fd, str.data(), str.length());
|
||||
close(fd);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
using PostActionBlock = bool (^)(SNTAction);
|
||||
using VerifyPostActionBlock = PostActionBlock (^)(SNTAction);
|
||||
|
||||
static const char *kExampleSigningID = "example.signing.id";
|
||||
static const char *kExampleTeamID = "myteamid";
|
||||
|
||||
VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction) {
|
||||
return ^bool(SNTAction gotAction) {
|
||||
XCTAssertEqual(gotAction, wantAction);
|
||||
@@ -186,7 +189,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)validateExecEvent:(SNTAction)wantAction {
|
||||
- (void)validateExecEvent:(SNTAction)wantAction
|
||||
messageSetup:(void (^)(es_message_t *))messageSetupBlock {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_file_t fileExec = MakeESFile("bar", {
|
||||
@@ -194,9 +198,14 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
.st_ino = 34,
|
||||
});
|
||||
es_process_t procExec = MakeESProcess(&fileExec);
|
||||
procExec.is_platform_binary = false;
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc);
|
||||
esMsg.event.exec.target = &procExec;
|
||||
|
||||
if (messageSetupBlock) {
|
||||
messageSetupBlock(&esMsg);
|
||||
}
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
@@ -208,6 +217,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)validateExecEvent:(SNTAction)wantAction {
|
||||
[self validateExecEvent:wantAction messageSetup:nil];
|
||||
}
|
||||
|
||||
- (void)testBinaryAllowRule {
|
||||
OCMStub([self.mockFileInfo isMachO]).andReturn(YES);
|
||||
OCMStub([self.mockFileInfo SHA256]).andReturn(@"a");
|
||||
@@ -215,7 +228,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
@@ -229,13 +245,102 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateBlock;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny];
|
||||
[self checkMetricCounters:kBlockBinary expected:@1];
|
||||
}
|
||||
|
||||
- (void)testSigningIDAllowRule {
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeSigningID;
|
||||
|
||||
NSString *signingID = [NSString stringWithFormat:@"%s:%s", kExampleTeamID, kExampleSigningID];
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:signingID
|
||||
certificateSHA256:nil
|
||||
teamID:@(kExampleTeamID)])
|
||||
.andReturn(rule);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kExampleSigningID);
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kExampleTeamID);
|
||||
}];
|
||||
[self checkMetricCounters:kAllowSigningID expected:@1];
|
||||
}
|
||||
|
||||
- (void)testSigningIDBlockRule {
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateBlock;
|
||||
rule.type = SNTRuleTypeSigningID;
|
||||
|
||||
NSString *signingID = [NSString stringWithFormat:@"%s:%s", kExampleTeamID, kExampleSigningID];
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:signingID
|
||||
certificateSHA256:nil
|
||||
teamID:@(kExampleTeamID)])
|
||||
.andReturn(rule);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kExampleSigningID);
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kExampleTeamID);
|
||||
}];
|
||||
[self checkMetricCounters:kBlockSigningID expected:@1];
|
||||
}
|
||||
|
||||
- (void)testTeamIDAllowRule {
|
||||
OCMStub([self.mockCodesignChecker signingInformation]).andReturn((@{
|
||||
(__bridge NSString *)kSecCodeInfoTeamIdentifier : @(kExampleTeamID),
|
||||
}));
|
||||
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeTeamID;
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@(kExampleTeamID)])
|
||||
.andReturn(rule);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kExampleTeamID);
|
||||
}];
|
||||
[self checkMetricCounters:kAllowTeamID expected:@1];
|
||||
}
|
||||
|
||||
- (void)testTeamIDBlockRule {
|
||||
OCMStub([self.mockCodesignChecker signingInformation]).andReturn((@{
|
||||
(__bridge NSString *)kSecCodeInfoTeamIdentifier : @(kExampleTeamID),
|
||||
}));
|
||||
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateBlock;
|
||||
rule.type = SNTRuleTypeTeamID;
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@(kExampleTeamID)])
|
||||
.andReturn(rule);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kExampleTeamID);
|
||||
}];
|
||||
[self checkMetricCounters:kBlockTeamID expected:@1];
|
||||
}
|
||||
|
||||
- (void)testCertificateAllowRule {
|
||||
OCMStub([self.mockFileInfo isMachO]).andReturn(YES);
|
||||
|
||||
@@ -246,7 +351,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeCertificate;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil certificateSHA256:@"a" teamID:nil])
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:@"a"
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
@@ -263,7 +371,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateBlock;
|
||||
rule.type = SNTRuleTypeCertificate;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil certificateSHA256:@"a" teamID:nil])
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:@"a"
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]);
|
||||
@@ -282,7 +393,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowCompiler;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllowCompiler];
|
||||
@@ -297,7 +411,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowCompiler;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
@@ -312,7 +429,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowTransitive;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
@@ -328,7 +448,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowTransitive;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]);
|
||||
@@ -439,7 +562,10 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil])
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
|
||||
@@ -44,10 +45,13 @@
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID;
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID;
|
||||
|
||||
/// Convenience initializer with nil hashes for both the file and certificate.
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo;
|
||||
/// Convenience initializer. Will obtain the teamID and construct the signingID
|
||||
/// identifier if able.
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
targetProcess:(nonnull const es_process_t *)targetProc;
|
||||
|
||||
///
|
||||
/// A wrapper for decisionForFileInfo:fileSHA256:certificateSHA256:. This method is slower as it
|
||||
@@ -58,6 +62,7 @@
|
||||
- (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID;
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID;
|
||||
|
||||
@end
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#import "Source/santad/SNTPolicyProcessor.h"
|
||||
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#import <Security/SecCode.h>
|
||||
|
||||
#include "Source/common/SNTLogging.h"
|
||||
|
||||
@@ -41,10 +42,12 @@
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID {
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID {
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.sha256 = fileSHA256 ?: fileInfo.SHA256;
|
||||
cd.teamID = teamID;
|
||||
cd.signingID = signingID;
|
||||
|
||||
// If the binary is a critical system binary, don't check its signature.
|
||||
// The binary was validated at startup when the rule table was initialized.
|
||||
@@ -66,13 +69,28 @@
|
||||
cd.certSHA256 = csInfo.leafCertificate.SHA256;
|
||||
cd.certCommonName = csInfo.leafCertificate.commonName;
|
||||
cd.certChain = csInfo.certificates;
|
||||
cd.teamID = teamID ?: [csInfo.signingInformation valueForKey:@"teamid"];
|
||||
cd.teamID = teamID
|
||||
?: [csInfo.signingInformation
|
||||
objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier];
|
||||
teamID = cd.teamID;
|
||||
|
||||
// Ensure that if no teamID exists that the signing info confirms it is a
|
||||
// platform binary. If not, remove the signingID.
|
||||
if (!teamID && signingID) {
|
||||
id platformID = [csInfo.signingInformation
|
||||
objectForKey:(__bridge NSString *)kSecCodeInfoPlatformIdentifier];
|
||||
if (![platformID isKindOfClass:[NSNumber class]] || [platformID intValue] == 0) {
|
||||
signingID = nil;
|
||||
}
|
||||
}
|
||||
|
||||
cd.signingID = signingID;
|
||||
}
|
||||
}
|
||||
cd.quarantineURL = fileInfo.quarantineDataURL;
|
||||
|
||||
SNTRule *rule = [self.ruleTable ruleForBinarySHA256:cd.sha256
|
||||
signingID:signingID
|
||||
certificateSHA256:cd.certSHA256
|
||||
teamID:teamID];
|
||||
if (rule) {
|
||||
@@ -108,6 +126,19 @@
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
case SNTRuleTypeSigningID:
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowSigningID; return cd;
|
||||
case SNTRuleStateSilentBlock:
|
||||
cd.silentBlock = YES;
|
||||
// intentional fallthrough
|
||||
case SNTRuleStateBlock:
|
||||
cd.customMsg = rule.customMsg;
|
||||
cd.decision = SNTEventStateBlockSigningID;
|
||||
return cd;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
case SNTRuleTypeCertificate:
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowCertificate; return cd;
|
||||
@@ -161,22 +192,47 @@
|
||||
return cd;
|
||||
}
|
||||
|
||||
switch ([[SNTConfigurator configurator] clientMode]) {
|
||||
SNTClientMode mode = [[SNTConfigurator configurator] clientMode];
|
||||
cd.decisionClientMode = mode;
|
||||
|
||||
switch (mode) {
|
||||
case SNTClientModeMonitor: cd.decision = SNTEventStateAllowUnknown; return cd;
|
||||
case SNTClientModeLockdown: cd.decision = SNTEventStateBlockUnknown; return cd;
|
||||
default: cd.decision = SNTEventStateBlockUnknown; return cd;
|
||||
}
|
||||
}
|
||||
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo {
|
||||
return [self decisionForFileInfo:fileInfo fileSHA256:nil certificateSHA256:nil teamID:nil];
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
targetProcess:(nonnull const es_process_t *)targetProc {
|
||||
NSString *signingID;
|
||||
NSString *teamID;
|
||||
|
||||
if (targetProc->signing_id.length > 0) {
|
||||
if (targetProc->team_id.length > 0) {
|
||||
teamID = [NSString stringWithUTF8String:targetProc->team_id.data];
|
||||
signingID =
|
||||
[NSString stringWithFormat:@"%@:%@", teamID,
|
||||
[NSString stringWithUTF8String:targetProc->signing_id.data]];
|
||||
} else if (targetProc->is_platform_binary) {
|
||||
signingID =
|
||||
[NSString stringWithFormat:@"platform:%@",
|
||||
[NSString stringWithUTF8String:targetProc->signing_id.data]];
|
||||
}
|
||||
}
|
||||
|
||||
return [self decisionForFileInfo:fileInfo
|
||||
fileSHA256:nil
|
||||
certificateSHA256:nil
|
||||
teamID:teamID
|
||||
signingID:signingID];
|
||||
}
|
||||
|
||||
// Used by `$ santactl fileinfo`.
|
||||
- (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID {
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID {
|
||||
SNTFileInfo *fileInfo;
|
||||
NSError *error;
|
||||
fileInfo = [[SNTFileInfo alloc] initWithPath:filePath error:&error];
|
||||
@@ -184,7 +240,8 @@
|
||||
return [self decisionForFileInfo:fileInfo
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID];
|
||||
teamID:teamID
|
||||
signingID:signingID];
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
@@ -43,6 +43,7 @@ using santa::santad::Metrics;
|
||||
using santa::santad::data_layer::WatchItems;
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::FlushCacheReason;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
@@ -129,46 +130,49 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
|
||||
SNTEndpointSecurityTamperResistance *tamper_client =
|
||||
[[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:esapi metrics:metrics logger:logger];
|
||||
|
||||
SNTEndpointSecurityFileAccessAuthorizer *access_authorizer_client =
|
||||
[[SNTEndpointSecurityFileAccessAuthorizer alloc] initWithESAPI:esapi
|
||||
metrics:metrics
|
||||
logger:logger
|
||||
watchItems:watch_items
|
||||
enricher:enricher
|
||||
decisionCache:[SNTDecisionCache sharedCache]];
|
||||
watch_items->RegisterClient(access_authorizer_client);
|
||||
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]];
|
||||
watch_items->RegisterClient(access_authorizer_client);
|
||||
}
|
||||
|
||||
EstablishSyncServiceConnection(syncd_queue);
|
||||
|
||||
NSArray<SNTKVOManager *> *kvoObservers = @[
|
||||
[[SNTKVOManager alloc] initWithObject:configurator
|
||||
selector:@selector(clientMode)
|
||||
type:[NSNumber class]
|
||||
callback:^(NSNumber *oldValue, NSNumber *newValue) {
|
||||
if ([oldValue longLongValue] == [newValue longLongValue]) {
|
||||
// Note: This case apparently can happen and if not checked
|
||||
// will result in excessive notification messages sent to the
|
||||
// user when calling `postClientModeNotification` below
|
||||
return;
|
||||
}
|
||||
NSMutableArray<SNTKVOManager *> *kvoObservers = [[NSMutableArray alloc] init];
|
||||
[kvoObservers addObjectsFromArray:@[
|
||||
[[SNTKVOManager alloc]
|
||||
initWithObject:configurator
|
||||
selector:@selector(clientMode)
|
||||
type:[NSNumber class]
|
||||
callback:^(NSNumber *oldValue, NSNumber *newValue) {
|
||||
if ([oldValue longLongValue] == [newValue longLongValue]) {
|
||||
// Note: This case apparently can happen and if not checked
|
||||
// will result in excessive notification messages sent to the
|
||||
// user when calling `postClientModeNotification` below
|
||||
return;
|
||||
}
|
||||
|
||||
SNTClientMode clientMode =
|
||||
(SNTClientMode)[newValue longLongValue];
|
||||
SNTClientMode clientMode = (SNTClientMode)[newValue longLongValue];
|
||||
|
||||
switch (clientMode) {
|
||||
case SNTClientModeLockdown:
|
||||
LOGI(@"Changed client mode to Lockdown, flushing cache.");
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
break;
|
||||
case SNTClientModeMonitor:
|
||||
LOGI(@"Changed client mode to Monitor.");
|
||||
break;
|
||||
default: LOGW(@"Changed client mode to unknown value."); break;
|
||||
}
|
||||
switch (clientMode) {
|
||||
case SNTClientModeLockdown:
|
||||
LOGI(@"Changed client mode to Lockdown, flushing cache.");
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches,
|
||||
FlushCacheReason::kClientModeChanged);
|
||||
break;
|
||||
case SNTClientModeMonitor: LOGI(@"Changed client mode to Monitor."); break;
|
||||
default: LOGW(@"Changed client mode to unknown value."); break;
|
||||
}
|
||||
|
||||
[[notifier_queue.notifierConnection remoteObjectProxy]
|
||||
postClientModeNotification:clientMode];
|
||||
}],
|
||||
[[notifier_queue.notifierConnection remoteObjectProxy]
|
||||
postClientModeNotification:clientMode];
|
||||
}],
|
||||
[[SNTKVOManager alloc]
|
||||
initWithObject:configurator
|
||||
selector:@selector(syncBaseURL)
|
||||
@@ -233,7 +237,8 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
|
||||
}
|
||||
|
||||
LOGI(@"Changed allowlist regex, flushing cache");
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches,
|
||||
FlushCacheReason::kPathRegexChanged);
|
||||
}],
|
||||
[[SNTKVOManager alloc]
|
||||
initWithObject:configurator
|
||||
@@ -246,7 +251,8 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
|
||||
}
|
||||
|
||||
LOGI(@"Changed denylist regex, flushing cache");
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches,
|
||||
FlushCacheReason::kPathRegexChanged);
|
||||
}],
|
||||
[[SNTKVOManager alloc] initWithObject:configurator
|
||||
selector:@selector(blockUSBMount)
|
||||
@@ -292,39 +298,15 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
|
||||
[newValue componentsJoinedByString:@","]);
|
||||
device_client.remountArgs = newValue;
|
||||
}],
|
||||
[[SNTKVOManager alloc]
|
||||
initWithObject:configurator
|
||||
selector:@selector(fileAccessPolicyPlist)
|
||||
type:[NSString class]
|
||||
callback:^(NSString *oldValue, NSString *newValue) {
|
||||
if ([configurator fileAccessPolicy]) {
|
||||
// Ignore any changes to this key if fileAccessPolicy is set
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldValue != newValue || (newValue && ![oldValue isEqualToString:newValue])) {
|
||||
LOGI(@"Filesystem monitoring policy config path changed: %@ -> %@", oldValue,
|
||||
newValue);
|
||||
watch_items->SetConfigPath(newValue);
|
||||
}
|
||||
}],
|
||||
[[SNTKVOManager alloc] initWithObject:configurator
|
||||
selector:@selector(fileAccessPolicy)
|
||||
type:[NSDictionary class]
|
||||
callback:^(NSDictionary *oldValue, NSDictionary *newValue) {
|
||||
if (oldValue != newValue ||
|
||||
(newValue && ![oldValue isEqualToDictionary:newValue])) {
|
||||
LOGI(@"Filesystem monitoring policy embedded config changed");
|
||||
watch_items->SetConfig(newValue);
|
||||
}
|
||||
}],
|
||||
[[SNTKVOManager alloc] initWithObject:configurator
|
||||
selector:@selector(staticRules)
|
||||
type:[NSArray class]
|
||||
callback:^(NSArray *oldValue, NSArray *newValue) {
|
||||
if ([oldValue isEqual:newValue]) return;
|
||||
LOGI(@"StaticRules set has changed, flushing cache.");
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
auth_result_cache->FlushCache(
|
||||
FlushCacheMode::kAllCaches,
|
||||
FlushCacheReason::kStaticRulesChanged);
|
||||
}],
|
||||
[[SNTKVOManager alloc]
|
||||
initWithObject:configurator
|
||||
@@ -356,7 +338,40 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
|
||||
// Forcefully exit. The daemon will be restarted immediately.
|
||||
exit(EXIT_SUCCESS);
|
||||
}],
|
||||
];
|
||||
]];
|
||||
|
||||
if (@available(macOS 13.0, *)) {
|
||||
// Only watch file access auth keys on mac 13 and newer
|
||||
[kvoObservers addObjectsFromArray:@[
|
||||
[[SNTKVOManager alloc]
|
||||
initWithObject:configurator
|
||||
selector:@selector(fileAccessPolicyPlist)
|
||||
type:[NSString class]
|
||||
callback:^(NSString *oldValue, NSString *newValue) {
|
||||
if ([configurator fileAccessPolicy]) {
|
||||
// Ignore any changes to this key if fileAccessPolicy is set
|
||||
return;
|
||||
}
|
||||
|
||||
if ((oldValue && !newValue) || (newValue && ![oldValue isEqualToString:newValue])) {
|
||||
LOGI(@"Filesystem monitoring policy config path changed: %@ -> %@", oldValue,
|
||||
newValue);
|
||||
watch_items->SetConfigPath(newValue);
|
||||
}
|
||||
}],
|
||||
[[SNTKVOManager alloc] initWithObject:configurator
|
||||
selector:@selector(fileAccessPolicy)
|
||||
type:[NSDictionary class]
|
||||
callback:^(NSDictionary *oldValue, NSDictionary *newValue) {
|
||||
if ((oldValue && !newValue) ||
|
||||
(newValue && ![oldValue isEqualToDictionary:newValue])) {
|
||||
LOGI(
|
||||
@"Filesystem monitoring policy embedded config changed");
|
||||
watch_items->SetConfig(newValue);
|
||||
}
|
||||
}],
|
||||
]];
|
||||
}
|
||||
|
||||
// Make the compiler happy. The variable is only used to ensure proper lifetime
|
||||
// of the SNTKVOManager objects it contains.
|
||||
|
||||
@@ -49,6 +49,8 @@ class SantadDeps {
|
||||
std::unique_ptr<santa::santad::logs::endpoint_security::Logger> logger,
|
||||
std::shared_ptr<santa::santad::Metrics> metrics,
|
||||
std::shared_ptr<santa::santad::data_layer::WatchItems> watch_items,
|
||||
std::shared_ptr<santa::santad::event_providers::AuthResultCache>
|
||||
auth_result_cache,
|
||||
MOLXPCConnection *control_connection,
|
||||
SNTCompilerController *compiler_controller,
|
||||
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "Source/santad/DataLayer/WatchItems.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#import "Source/santad/SNTDatabaseController.h"
|
||||
#include "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
using santa::common::PrefixTree;
|
||||
using santa::common::Unit;
|
||||
@@ -109,9 +110,10 @@ std::unique_ptr<SantadDeps> SantadDeps::Create(SNTConfigurator *configurator,
|
||||
size_t spool_dir_threshold_bytes = [configurator spoolDirectorySizeThresholdMB] * 1024 * 1024;
|
||||
uint64_t spool_flush_timeout_ms = [configurator spoolDirectoryEventMaxFlushTimeSec] * 1000;
|
||||
|
||||
std::unique_ptr<::Logger> logger = Logger::Create(
|
||||
esapi, [configurator eventLogType], [configurator eventLogPath], [configurator spoolDirectory],
|
||||
spool_dir_threshold_bytes, spool_file_threshold_bytes, spool_flush_timeout_ms);
|
||||
std::unique_ptr<::Logger> logger =
|
||||
Logger::Create(esapi, [configurator eventLogType], [SNTDecisionCache sharedCache],
|
||||
[configurator eventLogPath], [configurator spoolDirectory],
|
||||
spool_dir_threshold_bytes, spool_file_threshold_bytes, spool_flush_timeout_ms);
|
||||
if (!logger) {
|
||||
LOGE(@"Failed to create logger.");
|
||||
exit(EXIT_FAILURE);
|
||||
@@ -135,25 +137,31 @@ 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), control_connection,
|
||||
compiler_controller, notifier_queue, syncd_queue, exec_controller, prefix_tree);
|
||||
std::shared_ptr<::AuthResultCache> auth_result_cache = AuthResultCache::Create(esapi, metric_set);
|
||||
if (!auth_result_cache) {
|
||||
LOGE(@"Failed to create auth result cache");
|
||||
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);
|
||||
}
|
||||
|
||||
SantadDeps::SantadDeps(std::shared_ptr<EndpointSecurityAPI> esapi, std::unique_ptr<::Logger> logger,
|
||||
std::shared_ptr<::Metrics> metrics,
|
||||
std::shared_ptr<::WatchItems> watch_items,
|
||||
MOLXPCConnection *control_connection,
|
||||
SNTCompilerController *compiler_controller,
|
||||
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
|
||||
SNTExecutionController *exec_controller,
|
||||
std::shared_ptr<::PrefixTree<Unit>> prefix_tree)
|
||||
SantadDeps::SantadDeps(
|
||||
std::shared_ptr<EndpointSecurityAPI> esapi, std::unique_ptr<::Logger> logger,
|
||||
std::shared_ptr<::Metrics> metrics, std::shared_ptr<::WatchItems> watch_items,
|
||||
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)
|
||||
: esapi_(std::move(esapi)),
|
||||
logger_(std::move(logger)),
|
||||
metrics_(std::move(metrics)),
|
||||
watch_items_(std::move(watch_items)),
|
||||
enricher_(std::make_shared<::Enricher>()),
|
||||
auth_result_cache_(std::make_shared<::AuthResultCache>(esapi_)),
|
||||
auth_result_cache_(std::move(auth_result_cache)),
|
||||
control_connection_(control_connection),
|
||||
compiler_controller_(compiler_controller),
|
||||
notifier_queue_(notifier_queue),
|
||||
|
||||
@@ -37,6 +37,11 @@ using santa::santad::SantadDeps;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
static const char *kAllowedSigningID = "com.google.allowed_signing_id";
|
||||
static const char *kBlockedSigningID = "com.google.blocked_signing_id";
|
||||
static const char *kNoRuleMatchSigningID = "com.google.no_rule_match_signing_id";
|
||||
static const char *kBlockedTeamID = "EQHXZ8M8AV";
|
||||
static const char *kNoRuleMatchTeamID = "ABC1234XYZ";
|
||||
|
||||
@interface SantadTest : XCTestCase
|
||||
@property id mockSNTDatabaseController;
|
||||
@@ -56,7 +61,8 @@ NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
|
||||
- (BOOL)checkBinaryExecution:(NSString *)binaryName
|
||||
wantResult:(es_auth_result_t)wantResult
|
||||
clientMode:(NSInteger)clientMode {
|
||||
clientMode:(NSInteger)clientMode
|
||||
messageSetup:(void (^)(es_message_t *))messageSetupBlock {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
|
||||
@@ -102,6 +108,7 @@ NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
lstat(binaryPath.UTF8String, &fileStat);
|
||||
es_file_t file = MakeESFile([binaryPath UTF8String], fileStat);
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
proc.is_platform_binary = false;
|
||||
// Set a 6.5 second deadline for the message. The base SNTEndpointSecurityClient
|
||||
// class leaves a 5 second buffer to auto-respond to messages. A 6 second
|
||||
// deadline means there is a 1.5 second leeway given for the processing block
|
||||
@@ -111,6 +118,10 @@ NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth, 6500);
|
||||
esMsg.event.exec.target = &proc;
|
||||
|
||||
if (messageSetupBlock) {
|
||||
messageSetupBlock(&esMsg);
|
||||
}
|
||||
|
||||
// The test must wait for the ES client async message processing to complete.
|
||||
// Otherwise, the `es_message_t` stack variable will go out of scope and will
|
||||
// result in undefined behavior in the async dispatch queue block.
|
||||
@@ -146,6 +157,15 @@ NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (BOOL)checkBinaryExecution:(NSString *)binaryName
|
||||
wantResult:(es_auth_result_t)wantResult
|
||||
clientMode:(NSInteger)clientMode {
|
||||
return [self checkBinaryExecution:binaryName
|
||||
wantResult:wantResult
|
||||
clientMode:clientMode
|
||||
messageSetup:nil];
|
||||
}
|
||||
|
||||
/**
|
||||
* testRules ensures that we get the expected outcome when the mocks "execute"
|
||||
* our test binaries.
|
||||
@@ -211,6 +231,66 @@ NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
clientMode:SNTClientModeMonitor];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSigningIDBlockRuleAndCertAllowedRuleIsBlockedInMonitorMode {
|
||||
[self checkBinaryExecution:@"cert_hash_allowed_signingid_blocked"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeMonitor
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kBlockedSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSigningIDNoRuleMatchAndCertAllowedRuleIsAllowedInMonitorMode {
|
||||
[self checkBinaryExecution:@"cert_hash_allowed_signingid_not_matched"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSigningIDBlockRuleMatchAndCertAllowedRuleIsAllowedInMonitorMode {
|
||||
[self checkBinaryExecution:@"binary_hash_allowed_signingid_blocked"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kBlockedSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSigningIDNoRuleMatchIsAllowedInMonitorMode {
|
||||
[self checkBinaryExecution:@"noop"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kNoRuleMatchTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSigningIDNoRuleMatchIsBlockedInLockdownMode {
|
||||
[self checkBinaryExecution:@"noop"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeLockdown
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kNoRuleMatchTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithAllowedSigningIDRuleIsAllowedInLockdownMode {
|
||||
[self checkBinaryExecution:@"noop"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeLockdown
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->team_id = MakeESStringToken(kNoRuleMatchTeamID);
|
||||
msg->event.exec.target->signing_id = MakeESStringToken(kAllowedSigningID);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSHA256AllowRuleAndBlockedTeamIDRuleIsAllowedInLockdownMode {
|
||||
[self checkBinaryExecution:@"banned_teamid_allowed_binary"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
|
||||
BIN
Source/santad/testdata/binaryrules/binary_hash_allowed_signingid_blocked
vendored
Executable file
BIN
Source/santad/testdata/binaryrules/binary_hash_allowed_signingid_blocked
vendored
Executable file
Binary file not shown.
BIN
Source/santad/testdata/binaryrules/cert_hash_allowed_signingid_blocked
vendored
Executable file
BIN
Source/santad/testdata/binaryrules/cert_hash_allowed_signingid_blocked
vendored
Executable file
Binary file not shown.
BIN
Source/santad/testdata/binaryrules/cert_hash_allowed_signingid_not_matched
vendored
Executable file
BIN
Source/santad/testdata/binaryrules/cert_hash_allowed_signingid_not_matched
vendored
Executable file
Binary file not shown.
BIN
Source/santad/testdata/binaryrules/rules.db
vendored
BIN
Source/santad/testdata/binaryrules/rules.db
vendored
Binary file not shown.
@@ -101,6 +101,7 @@
|
||||
break;
|
||||
case SNTEventStateAllowScope: ADDKEY(newEvent, kDecision, kDecisionAllowScope); break;
|
||||
case SNTEventStateAllowTeamID: ADDKEY(newEvent, kDecision, kDecisionAllowTeamID); break;
|
||||
case SNTEventStateAllowSigningID: ADDKEY(newEvent, kDecision, kDecisionAllowSigningID); break;
|
||||
case SNTEventStateBlockUnknown: ADDKEY(newEvent, kDecision, kDecisionBlockUnknown); break;
|
||||
case SNTEventStateBlockBinary: ADDKEY(newEvent, kDecision, kDecisionBlockBinary); break;
|
||||
case SNTEventStateBlockCertificate:
|
||||
@@ -108,6 +109,7 @@
|
||||
break;
|
||||
case SNTEventStateBlockScope: ADDKEY(newEvent, kDecision, kDecisionBlockScope); break;
|
||||
case SNTEventStateBlockTeamID: ADDKEY(newEvent, kDecision, kDecisionBlockTeamID); break;
|
||||
case SNTEventStateBlockSigningID: ADDKEY(newEvent, kDecision, kDecisionBlockSigningID); break;
|
||||
case SNTEventStateBundleBinary:
|
||||
ADDKEY(newEvent, kDecision, kDecisionBundleBinary);
|
||||
[newEvent removeObjectForKey:kExecutionTime];
|
||||
@@ -150,6 +152,7 @@
|
||||
}
|
||||
newEvent[kSigningChain] = signingChain;
|
||||
ADDKEY(newEvent, kTeamID, event.teamID);
|
||||
ADDKEY(newEvent, kSigningID, event.signingID);
|
||||
|
||||
return newEvent;
|
||||
#undef ADDKEY
|
||||
|
||||
@@ -54,6 +54,7 @@ static const uint8_t kMaxEnqueuedSyncs = 2;
|
||||
@property NSUInteger eventBatchSize;
|
||||
|
||||
@property NSString *xsrfToken;
|
||||
@property NSString *xsrfTokenHeader;
|
||||
|
||||
@end
|
||||
|
||||
@@ -125,6 +126,7 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
|
||||
[self startReachability];
|
||||
}
|
||||
self.xsrfToken = syncState.xsrfToken;
|
||||
self.xsrfTokenHeader = syncState.xsrfTokenHeader;
|
||||
}
|
||||
|
||||
- (void)postBundleEventToSyncServer:(SNTStoredEvent *)event
|
||||
@@ -158,6 +160,7 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
|
||||
[self startReachability];
|
||||
}
|
||||
self.xsrfToken = syncState.xsrfToken;
|
||||
self.xsrfTokenHeader = syncState.xsrfTokenHeader;
|
||||
}
|
||||
|
||||
- (void)isFCMListening:(void (^)(BOOL))reply {
|
||||
@@ -229,6 +232,7 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
|
||||
BOOL ret = [p sync];
|
||||
LOGD(@"Rule download %@", ret ? @"complete" : @"failed");
|
||||
self.xsrfToken = syncState.xsrfToken;
|
||||
self.xsrfTokenHeader = syncState.xsrfTokenHeader;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -254,6 +258,7 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
|
||||
if ([p sync]) {
|
||||
SLOGD(@"Preflight complete");
|
||||
self.xsrfToken = syncState.xsrfToken;
|
||||
self.xsrfTokenHeader = syncState.xsrfTokenHeader;
|
||||
|
||||
// Clean up reachability if it was started for a non-network error
|
||||
[self stopReachability];
|
||||
@@ -355,6 +360,7 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
|
||||
}
|
||||
|
||||
syncState.xsrfToken = self.xsrfToken;
|
||||
syncState.xsrfTokenHeader = self.xsrfTokenHeader;
|
||||
|
||||
NSURLSessionConfiguration *sessConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
|
||||
sessConfig.connectionProxyDictionary = [[SNTConfigurator configurator] syncProxyConfig];
|
||||
@@ -391,10 +397,7 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
|
||||
|
||||
syncState.session = [authURLSession session];
|
||||
syncState.daemonConn = self.daemonConn;
|
||||
|
||||
syncState.compressedContentEncoding =
|
||||
config.enableBackwardsCompatibleContentEncoding ? @"zlib" : @"deflate";
|
||||
|
||||
syncState.contentEncoding = config.syncClientContentEncoding;
|
||||
syncState.pushNotificationsToken = self.pushNotifications.token;
|
||||
|
||||
return syncState;
|
||||
|
||||
@@ -49,12 +49,13 @@
|
||||
// dispatch_group_t group = dispatch_group_create();
|
||||
// dispatch_group_enter(group);
|
||||
[rop databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive, int64_t teamID) {
|
||||
int64_t transitive, int64_t teamID, int64_t signingID) {
|
||||
requestDict[kBinaryRuleCount] = @(binary);
|
||||
requestDict[kCertificateRuleCount] = @(certificate);
|
||||
requestDict[kCompilerRuleCount] = @(compiler);
|
||||
requestDict[kTransitiveRuleCount] = @(transitive);
|
||||
requestDict[kTeamIDRuleCount] = @(teamID);
|
||||
requestDict[kSigningIDRuleCount] = @(signingID);
|
||||
}];
|
||||
|
||||
[rop clientMode:^(SNTClientMode cm) {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santasyncservice/SNTSyncStage.h"
|
||||
#include "Source/common/SNTCommonEnums.h"
|
||||
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
|
||||
@@ -70,10 +71,27 @@
|
||||
NSString *xsrfHeader = self.syncState.xsrfTokenHeader ?: kDefaultXSRFTokenHeader;
|
||||
[req setValue:self.syncState.xsrfToken forHTTPHeaderField:xsrfHeader];
|
||||
|
||||
NSData *compressed = [requestBody zlibCompressed];
|
||||
NSData *compressed;
|
||||
NSString *contentEncodingHeader;
|
||||
|
||||
switch (self.syncState.contentEncoding) {
|
||||
case SNTSyncContentEncodingNone: break;
|
||||
case SNTSyncContentEncodingGzip:
|
||||
compressed = [requestBody gzipCompressed];
|
||||
contentEncodingHeader = @"gzip";
|
||||
break;
|
||||
case SNTSyncContentEncodingDeflate:
|
||||
compressed = [requestBody zlibCompressed];
|
||||
contentEncodingHeader = @"deflate";
|
||||
break;
|
||||
default:
|
||||
// This would be a programming error.
|
||||
LOGD(@"Unexpected value for content encoding %ld", self.syncState.contentEncoding);
|
||||
}
|
||||
|
||||
if (compressed) {
|
||||
requestBody = compressed;
|
||||
[req setValue:self.syncState.compressedContentEncoding forHTTPHeaderField:@"Content-Encoding"];
|
||||
[req setValue:contentEncodingHeader forHTTPHeaderField:@"Content-Encoding"];
|
||||
}
|
||||
|
||||
[req setHTTPBody:requestBody];
|
||||
@@ -183,9 +201,15 @@
|
||||
}
|
||||
|
||||
- (NSData *)stripXssi:(NSData *)data {
|
||||
static const char xssi[3] = {']', ')', '}'};
|
||||
if (data.length < 3 || strncmp(data.bytes, xssi, 3)) return data;
|
||||
return [data subdataWithRange:NSMakeRange(3, data.length - 3)];
|
||||
static const char xssiOne[5] = {')', ']', '}', '\'', '\n'};
|
||||
static const char xssiTwo[3] = {']', ')', '}'};
|
||||
if (data.length >= sizeof(xssiOne) && strncmp(data.bytes, xssiOne, sizeof(xssiOne)) == 0) {
|
||||
return [data subdataWithRange:NSMakeRange(sizeof(xssiOne), data.length - sizeof(xssiOne))];
|
||||
}
|
||||
if (data.length >= sizeof(xssiTwo) && strncmp(data.bytes, xssiTwo, sizeof(xssiTwo)) == 0) {
|
||||
return [data subdataWithRange:NSMakeRange(sizeof(xssiTwo), data.length - sizeof(xssiTwo))];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
- (BOOL)fetchXSRFToken {
|
||||
|
||||
@@ -74,8 +74,7 @@
|
||||
/// Array of bundle IDs to find binaries for.
|
||||
@property NSArray *bundleBinaryRequests;
|
||||
|
||||
/// The header value for ContentEncoding when sending compressed content.
|
||||
/// Either "deflate" (default) or "zlib".
|
||||
@property(copy) NSString *compressedContentEncoding;
|
||||
/// The content-encoding to use for the client uploads during the sync session.
|
||||
@property SNTSyncContentEncoding contentEncoding;
|
||||
|
||||
@end
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
}
|
||||
@end
|
||||
|
||||
@interface SNTSyncStage (XSSI)
|
||||
- (NSData *)stripXssi:(NSData *)data;
|
||||
@end
|
||||
|
||||
@interface SNTSyncTest : XCTestCase
|
||||
@property SNTSyncState *syncState;
|
||||
@property id<SNTDaemonControlXPC> daemonConnRop;
|
||||
@@ -148,6 +152,7 @@
|
||||
OCMOCK_VALUE(0), // compiler
|
||||
OCMOCK_VALUE(0), // transitive
|
||||
OCMOCK_VALUE(0), // teamID
|
||||
OCMOCK_VALUE(0), // signingID
|
||||
nil])]);
|
||||
OCMStub([self.daemonConnRop syncCleanRequired:([OCMArg invokeBlockWithArgs:@NO, nil])]);
|
||||
OCMStub([self.daemonConnRop
|
||||
@@ -156,6 +161,27 @@
|
||||
|
||||
#pragma mark - SNTSyncStage Tests
|
||||
|
||||
- (void)testStripXssi {
|
||||
SNTSyncStage *sut = [[SNTSyncStage alloc] initWithState:self.syncState];
|
||||
|
||||
char wantChar[3] = {'"', 'a', '"'};
|
||||
NSData *want = [NSData dataWithBytes:wantChar length:3];
|
||||
|
||||
char dOne[8] = {')', ']', '}', '\'', '\n', '"', 'a', '"'};
|
||||
XCTAssertEqualObjects([sut stripXssi:[NSData dataWithBytes:dOne length:8]], want, @"");
|
||||
|
||||
char dTwo[6] = {']', ')', '}', '"', 'a', '"'};
|
||||
XCTAssertEqualObjects([sut stripXssi:[NSData dataWithBytes:dTwo length:6]], want, @"");
|
||||
|
||||
char dThree[5] = {')', ']', '}', '\'', '\n'};
|
||||
XCTAssertEqualObjects([sut stripXssi:[NSData dataWithBytes:dThree length:5]], [NSData data], @"");
|
||||
|
||||
char dFour[3] = {']', ')', '}'};
|
||||
XCTAssertEqualObjects([sut stripXssi:[NSData dataWithBytes:dFour length:3]], [NSData data], @"");
|
||||
|
||||
XCTAssertEqualObjects([sut stripXssi:want], want, @"");
|
||||
}
|
||||
|
||||
- (void)testBaseFetchXSRFTokenSuccess {
|
||||
// NOTE: This test only works if the other tests don't return a 403 and run before this test.
|
||||
// The XSRF fetching code is inside a dispatch_once.
|
||||
@@ -295,12 +321,17 @@
|
||||
- (void)testPreflightDatabaseCounts {
|
||||
SNTSyncPreflight *sut = [[SNTSyncPreflight alloc] initWithState:self.syncState];
|
||||
|
||||
int64_t bin = 5, cert = 8, compiler = 2, transitive = 19, teamID = 3;
|
||||
int64_t bin = 5;
|
||||
int64_t cert = 8;
|
||||
int64_t compiler = 2;
|
||||
int64_t transitive = 19;
|
||||
int64_t teamID = 3;
|
||||
int64_t signingID = 123;
|
||||
OCMStub([self.daemonConnRop
|
||||
databaseRuleCounts:([OCMArg invokeBlockWithArgs:OCMOCK_VALUE(bin), OCMOCK_VALUE(cert),
|
||||
OCMOCK_VALUE(compiler),
|
||||
OCMOCK_VALUE(transitive), OCMOCK_VALUE(teamID),
|
||||
nil])]);
|
||||
OCMOCK_VALUE(signingID), nil])]);
|
||||
|
||||
[self stubRequestBody:nil
|
||||
response:nil
|
||||
@@ -312,6 +343,7 @@
|
||||
XCTAssertEqualObjects(requestDict[kCompilerRuleCount], @(2));
|
||||
XCTAssertEqualObjects(requestDict[kTransitiveRuleCount], @(19));
|
||||
XCTAssertEqualObjects(requestDict[kTeamIDRuleCount], @(3));
|
||||
XCTAssertEqualObjects(requestDict[kSigningIDRuleCount], @(123));
|
||||
return YES;
|
||||
}];
|
||||
|
||||
@@ -327,6 +359,7 @@
|
||||
OCMOCK_VALUE(0), // compiler
|
||||
OCMOCK_VALUE(0), // transitive
|
||||
OCMOCK_VALUE(0), // teamID
|
||||
OCMOCK_VALUE(0), // signingID
|
||||
nil])]);
|
||||
OCMStub([self.daemonConnRop
|
||||
clientMode:([OCMArg invokeBlockWithArgs:OCMOCK_VALUE(SNTClientModeMonitor), nil])]);
|
||||
@@ -408,6 +441,7 @@
|
||||
XCTAssertEqualObjects(cert[kCertValidUntil], @(1618266875));
|
||||
|
||||
XCTAssertEqualObjects(event[kTeamID], @"012345678910");
|
||||
XCTAssertEqualObjects(event[kSigningID], @"signing.id");
|
||||
|
||||
event = events[1];
|
||||
XCTAssertEqualObjects(event[kFileName], @"hub");
|
||||
|
||||
@@ -96,6 +96,11 @@
|
||||
<key>CF$UID</key>
|
||||
<integer>39</integer>
|
||||
</dict>
|
||||
<key>signingID</key>
|
||||
<dict>
|
||||
<key>CF$UID</key>
|
||||
<integer>40</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<integer>14887</integer>
|
||||
<string>ff98fa0c0a1095fedcbe4d388a9760e71399a5c3c017a847ffa545663b57929a</string>
|
||||
@@ -398,7 +403,8 @@
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<string>012345678910</string>
|
||||
<string>012345678910</string>
|
||||
<string>signing.id</string>
|
||||
</array>
|
||||
<key>$top</key>
|
||||
<dict>
|
||||
|
||||
@@ -30,6 +30,7 @@ also known as mobileconfig files, which are in an Apple-specific XML format.
|
||||
| EnableBadSignatureProtection | Bool | Enable bad signature protection, defaults to NO. If this flag is set to YES, binaries with a bad signing chain will be blocked even in MONITOR mode, **unless** the binary is allowed by an explicit rule. |
|
||||
| EnablePageZeroProtection | Bool | Enable `__PAGEZERO` protection, defaults to YES. If this flag is set to YES, 32-bit binaries that are missing the `__PAGEZERO` segment will be blocked even in MONITOR mode, **unless** the binary is allowed by an explicit rule. |
|
||||
| EnableSilentMode | Bool | If true, Santa will not post any GUI notifications. This can be a very confusing experience for users, use with caution. Defaults to NO. |
|
||||
| EnableSilentTTYMode | Bool | If true, Santa will not post any TTY notifications. This can be a very confusing experience for users, use with caution. Defaults to NO. |
|
||||
| AboutText | String | The text to display when the user opens Santa.app. If unset, the default text will be displayed. |
|
||||
| MoreInfoURL | String | The URL to open when the user clicks "More Info..." when opening Santa.app. If unset, the button will not be displayed. |
|
||||
| EventDetailURL | String | See the [EventDetailURL](#eventdetailurl) section below. |
|
||||
@@ -71,6 +72,7 @@ also known as mobileconfig files, which are in an Apple-specific XML format.
|
||||
| RemountUSBMode | Array | Array of strings for arguments to pass to mount -o (any of "rdonly", "noexec", "nosuid", "nobrowse", "noowners", "nodev", "async", "-j"). when forcibly remounting devices. No default. |
|
||||
| FileAccessPolicyPlist | String | (BETA) Path to a file access configuration plist. |
|
||||
| FileAccessPolicyUpdateIntervalSec | Integer | (BETA) Number of seconds between re-reading the file access policy config and policies/monitored paths updated. |
|
||||
| SyncClientContentEncoding | String | Sets the Content-Encoding header for requests sent to the sync service. Acceptable values are "deflate", "gzip", "none" (Defaults to deflate.) |
|
||||
|
||||
|
||||
\*overridable by the sync server: run `santactl status` to check the current
|
||||
|
||||
@@ -18,24 +18,25 @@ To enable this feature, the `FileAccessPolicyPlist` key in the main [Santa confi
|
||||
|
||||
## Configuration
|
||||
|
||||
| Key | Parent | Type | Required | Santa Version | Description |
|
||||
| :------------------ | :----------- | :--------- | :------- | :------------ | :---------- |
|
||||
| `Version` | `<Root>` | String | Yes | v2023.1+ | Version of the configuration. Will be reported in events. |
|
||||
| `WatchItems` | `<Root>` | Dictionary | No | v2023.1+ | The set of configuration items that will be monitored by Santa. |
|
||||
| `<Name>` | `WatchItems` | Dictionary | No | v2023.1+ | A unique name that identifies a single watch item rule. This value will be reported in events. The name must be a legal C identifier (i.e., must conform to the regex `[A-Za-z_][A-Za-z0-9_]*`). |
|
||||
| `Paths` | `<Name>` | Array | Yes | v2023.1+ | A list of either String or Dictionary types that contain path globs to monitor. String type entires will have default values applied for the attributes that can be manually set with the Dictionary type. |
|
||||
| `Path` | `Paths` | String | Yes | v2023.1+ | The path glob to monitor. |
|
||||
| `IsPrefix` | `Paths` | Boolean | No | v2023.1+ | Whether or not the path glob represents a prefix path. (Default = `false`) |
|
||||
| `Options` | `<Name>` | Dictionary | No | v2023.1+ | Customizes the actions for a given rule. |
|
||||
| `AllowReadAccess` | `Options` | Boolean | No | v2023.1+ | If true, indicates the rule will **not** be applied to actions that are read-only access (e.g., opening a watched path for reading, or cloning a watched path). If false, the rule will apply both to read-only access and access that could modify the watched path. (Default = `false`) |
|
||||
| `AuditOnly` | `Options` | Boolean | No | v2023.1+ | If true, operations violating the rule will only be logged. If false, operations violating the rule will be denied and logged. (Default = `true`) |
|
||||
| `Processes` | `<Name>` | Array | No | v2023.1+ | A list of dictionaries defining processes that are allowed to access paths matching the globs defined with the `Paths` key. For a process performing the operation to be considered a match, it must match all defined attributes of at least one entry in the list. |
|
||||
| `BinaryPath` | `Processes` | String | No | v2023.1+ | A path literal that an instigating process must be executed from. |
|
||||
| `TeamID` | `Processes` | String | No | v2023.1+ | Team ID of the instigating process. |
|
||||
| `CertificateSha256` | `Processes` | String | No | v2023.1+ | SHA256 of the leaf certificate of the instigating process. |
|
||||
| `CDHash` | `Processes` | String | No | v2023.1+ | CDHash of the instigating process. |
|
||||
| `SigningID` | `Processes` | String | No | v2023.1+ | Signing ID of the instigating process. |
|
||||
| `PlatformBinary` | `Processes` | Boolean | No | v2023.2+ | Whether or not the instigating process is a platform binary. |
|
||||
| Key | Parent | Type | Required | Santa Version | Description |
|
||||
| :------------------------ | :----------- | :--------- | :------- | :------------ | :---------- |
|
||||
| `Version` | `<Root>` | String | Yes | v2023.1+ | Version of the configuration. Will be reported in events. |
|
||||
| `WatchItems` | `<Root>` | Dictionary | No | v2023.1+ | The set of configuration items that will be monitored by Santa. |
|
||||
| `<Name>` | `WatchItems` | Dictionary | No | v2023.1+ | A unique name that identifies a single watch item rule. This value will be reported in events. The name must be a legal C identifier (i.e., must conform to the regex `[A-Za-z_][A-Za-z0-9_]*`). |
|
||||
| `Paths` | `<Name>` | Array | Yes | v2023.1+ | A list of either String or Dictionary types that contain path globs to monitor. String type entires will have default values applied for the attributes that can be manually set with the Dictionary type. |
|
||||
| `Path` | `Paths` | String | Yes | v2023.1+ | The path glob to monitor. |
|
||||
| `IsPrefix` | `Paths` | Boolean | No | v2023.1+ | Whether or not the path glob represents a prefix path. (Default = `false`) |
|
||||
| `Options` | `<Name>` | Dictionary | No | v2023.1+ | Customizes the actions for a given rule. |
|
||||
| `AllowReadAccess` | `Options` | Boolean | No | v2023.1+ | If true, indicates the rule will **not** be applied to actions that are read-only access (e.g., opening a watched path for reading, or cloning a watched path). If false, the rule will apply both to read-only access and access that could modify the watched path. (Default = `false`) |
|
||||
| `AuditOnly` | `Options` | Boolean | No | v2023.1+ | If true, operations violating the rule will only be logged. If false, operations violating the rule will be denied and logged. (Default = `true`) |
|
||||
| `InvertProcessExceptions` | `Options` | Boolean | No | v2023.5+ | If true, logic is inverted for the list of processes defined by the `Processes` key such that the list becomes the set of processes that will be denied or allowed but audited. (Default = `false`) |
|
||||
| `Processes` | `<Name>` | Array | No | v2023.1+ | A list of dictionaries defining processes that are allowed to access paths matching the globs defined with the `Paths` key. For a process performing the operation to be considered a match, it must match all defined attributes of at least one entry in the list. |
|
||||
| `BinaryPath` | `Processes` | String | No | v2023.1+ | A path literal that an instigating process must be executed from. |
|
||||
| `TeamID` | `Processes` | String | No | v2023.1+ | Team ID of the instigating process. |
|
||||
| `CertificateSha256` | `Processes` | String | No | v2023.1+ | SHA256 of the leaf certificate of the instigating process. |
|
||||
| `CDHash` | `Processes` | String | No | v2023.1+ | CDHash of the instigating process. |
|
||||
| `SigningID` | `Processes` | String | No | v2023.1+ | Signing ID of the instigating process. |
|
||||
| `PlatformBinary` | `Processes` | Boolean | No | v2023.2+ | Whether or not the instigating process is a platform binary. |
|
||||
|
||||
### Example Configuration
|
||||
|
||||
@@ -67,6 +68,8 @@ This is an example configuration conforming to the specification outlined above:
|
||||
<false/>
|
||||
<key>AuditOnly</key>
|
||||
<true/>
|
||||
<key>InvertProcessExceptions</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>Processes</key>
|
||||
<array>
|
||||
@@ -170,3 +173,7 @@ action=FILE_ACCESS|policy_version=v0.1-experimental|policy_name=UserFoo|path=/Us
|
||||
```
|
||||
|
||||
When the `EventLogType` configuration key is set to `protobuf`, a log is emitted to match the `FileAccess` message in the [santa.proto](https://github.com/google/santa/blob/main/Source/common/santa.proto) schema.
|
||||
|
||||
### Default Mute Set
|
||||
|
||||
The EndpointSecurity framework maintains a set of paths dubbed the "default mute set" that are particularly difficult for ES clients to handle. Additionally, AUTH events from some of these paths have ES response deadline times set very low. In order to help increase stability of this feature, file accesses from binaries in the default mute set are not currently logged. A list of binaries that will not have operations logged can be found in [SNTRuleTable.m](https://github.com/google/santa/blob/2023.4/Source/santad/DataLayer/SNTRuleTable.m#L90-L105). This could be addressed in the future (see [Github Issue #1096](https://github.com/google/santa/issues/1096)).
|
||||
|
||||
@@ -128,6 +128,31 @@ Run all the logic / unit tests
|
||||
bazel test :unit_tests --define=SANTA_BUILD_TYPE=adhoc --test_output=errors
|
||||
```
|
||||
|
||||
##### Testing Config Options Using `/var/db/santa/config-overrides.plist`
|
||||
|
||||
Debug versions of Santa have the ability to set/override config settings using an override file, that will be applied over the top of the configuration from a profile.
|
||||
|
||||
1. Create a plist in `/var/db/santa/config-overrides.plist`
|
||||
|
||||
For example to point Santa at a sync server running on localhost here would be the config-override file.
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SyncBaseURL</key>
|
||||
<string>http://localhost:8080/v1/santa/</string>
|
||||
<key>SyncClientContentEncoding</key>
|
||||
<string>gzip</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
> :warning: Warning
|
||||
> Make sure the file is readable.
|
||||
|
||||
2. run `bazel run //:reload` to rebuild and restart the Santa daemon.
|
||||
|
||||
#### Releases
|
||||
|
||||
Creates a release build of Santa with a version based of the newest tag. Also
|
||||
@@ -136,5 +161,5 @@ interpreting future crashes much easier. Releases are handled by Google internal
|
||||
infrastructure.
|
||||
|
||||
```sh
|
||||
bazel build --apple_generate_dsym -c opt :release
|
||||
bazel build --apple_generate_dsym -c opt //:release
|
||||
```
|
||||
|
||||
@@ -98,7 +98,7 @@ The request consists of the following JSON keys:
|
||||
| transitive_rule_count | NO | int | Number of transitive rules the client has at the time of sync |
|
||||
| teamid_rule_count | NO | int | Number of TeamID rules the client has at the time of sync | 24 |
|
||||
| client_mode | YES | string | the mode the client is operating in, either "LOCKDOWN" or "MONITOR" | LOCKDOWN |
|
||||
| clean_sync | NO | bool | the client has requested that a clean sync of its rules from the server. | true |
|
||||
| request_clean_sync | NO | bool | the client has requested a clean sync of its rules from the server. | true |
|
||||
|
||||
|
||||
### Example preflight request JSON Payload:
|
||||
@@ -118,7 +118,7 @@ The request consists of the following JSON keys:
|
||||
"transitive_rule_count" : 0,
|
||||
"os_version" : "12.4",
|
||||
"model_identifier" : "MacBookPro15,1",
|
||||
"clean_sync": true,
|
||||
"request_clean_sync": true,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -186,7 +186,7 @@ sequenceDiagram
|
||||
| file_path | YES | string | Absolute file path to the executable that was blocked | "/tmp/foo" |
|
||||
| file_name | YES | string | Command portion of the path of the blocked executable | "foo" |
|
||||
| executing_user | YES | string | Username that executed the binary | "markowsky" |
|
||||
| execution_time | YES | int | Unix timestamp of when the execution occured | 23344234232 |
|
||||
| execution_time | YES | float64 | Unix timestamp of when the execution occured | 23344234232 |
|
||||
| loggedin_users | NO | List of strings | list of usernames logged in according to utmp | ["markowsky"] |
|
||||
| current_sessions | YES | List of strings | list of user sessions | ["markowsky@console", "markowsky@ttys000"] |
|
||||
| decision | YES | string | The decision Santa made for this binary, BUNDLE_BINARY is used to preemptively report binaries in a bundle. **Must be one of the examples**.| "ALLOW_BINARY", "ALLOW_CERTIFICATE", "ALLOW_SCOPE", "ALLOW_TEAMID", "ALLOW_UNKNOWN", "BLOCK_BINARY", "BLOCK_CERTIFICATE", "BLOCK_SCOPE", "BLOCK_TEAMID", "BLOCK_UNKNOWN", "BUNDLE_BINARY" |
|
||||
@@ -197,13 +197,13 @@ sequenceDiagram
|
||||
| file_bundle_version | NO | string | The bundle version string | "9999.1.1" |
|
||||
| file_bundle_version_string | NO | string | Bundle short version string | "2.3.4" |
|
||||
| file_bundle_hash | NO | string | SHA256 hash of all executables in the bundle | "7466e3687f540bcb7792c6d14d5a186667dbe18a85021857b42effe9f0370805" |
|
||||
| file_bundle_hash_millis | NO | int | The time in milliseconds it took to find all of the binaries, hash and produce the bundle_hash | 1234775 |
|
||||
| file_bundle_hash_millis | NO | float64 | The time in milliseconds it took to find all of the binaries, hash and produce the bundle_hash | 1234775 |
|
||||
| pid | YES | int | Process id of the executable that was blocked | 1234 |
|
||||
| ppid | YES | int | Parent process id of the executable that was blocked | 456 |
|
||||
| parent_name | YES | Parent process short command name of the executable that was blocked | "bar" |
|
||||
| quarantine_data_url | NO | string | The actual URL of the quarantined item from the quarantine database that this binary was downloaded from | https://dl.google.com/chrome/mac/stable/GGRO/googlechrome.dmg |
|
||||
| quarantine_referer_url | NO | string | Referring URL that lead to the binary being downloaded if known. | https://www.google.com/chrome/downloads/ |
|
||||
| quarantine_timestamp | NO | int | Unix Timestamp of when the binary was downloaded or 0 if not quarantined | 0 |
|
||||
| quarantine_timestamp | NO | float64 | Unix Timestamp of when the binary was downloaded or 0 if not quarantined | 0 |
|
||||
| quarantine_agent_bundle_id | NO | string | The bundle ID of the software that quarantined the binary | "com.apple.Safari" |
|
||||
| signing_chain | NO | list of signing chain objects | Certs used to code sign the executable | See next section |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user