mirror of
https://github.com/google/santa.git
synced 2026-01-15 01:08:12 -05:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57fc2b0253 | ||
|
|
262adfecbd | ||
|
|
1606657bb3 | ||
|
|
b379819cfa | ||
|
|
b9f6005411 | ||
|
|
e31aa5cf39 | ||
|
|
77d191ae26 | ||
|
|
160195a1d4 | ||
|
|
f2ce92650b | ||
|
|
e89cdbcf64 | ||
|
|
6a697e00ea | ||
|
|
74d8fe30d1 | ||
|
|
7513c75f88 | ||
|
|
9bee43130e | ||
|
|
7fa23d4b97 | ||
|
|
42eb0a3669 | ||
|
|
1ea26f0ac9 | ||
|
|
c35e9978d3 | ||
|
|
e4c0d56bb6 | ||
|
|
908b1bcabe | ||
|
|
64e81bedc6 | ||
|
|
5dfab22fa7 | ||
|
|
5248e2a7eb | ||
|
|
e8db89c57c |
@@ -11,6 +11,7 @@ proto_library(
|
||||
name = "santa_proto",
|
||||
srcs = ["santa.proto"],
|
||||
deps = [
|
||||
"//Source/santad/ProcessTree:process_tree_proto",
|
||||
"@com_google_protobuf//:any_proto",
|
||||
"@com_google_protobuf//:timestamp_proto",
|
||||
],
|
||||
@@ -263,6 +264,7 @@ objc_library(
|
||||
hdrs = ["SNTFileInfo.h"],
|
||||
deps = [
|
||||
":SNTLogging",
|
||||
":SantaVnode",
|
||||
"@FMDB",
|
||||
"@MOLCodesignChecker",
|
||||
],
|
||||
@@ -312,6 +314,12 @@ santa_unit_test(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTRuleIdentifiers",
|
||||
srcs = ["SNTRuleIdentifiers.m"],
|
||||
hdrs = ["SNTRuleIdentifiers.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTStoredEvent",
|
||||
srcs = ["SNTStoredEvent.m"],
|
||||
@@ -377,6 +385,7 @@ objc_library(
|
||||
":SNTCommonEnums",
|
||||
":SNTConfigurator",
|
||||
":SNTRule",
|
||||
":SNTRuleIdentifiers",
|
||||
":SNTStoredEvent",
|
||||
":SNTXPCUnprivilegedControlInterface",
|
||||
"@MOLCodesignChecker",
|
||||
@@ -419,6 +428,7 @@ objc_library(
|
||||
deps = [
|
||||
":SNTCommonEnums",
|
||||
":SNTRule",
|
||||
":SNTRuleIdentifiers",
|
||||
":SNTStoredEvent",
|
||||
":SNTXPCBundleServiceInterface",
|
||||
":SantaVnode",
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
///
|
||||
@interface SNTCachedDecision : NSObject
|
||||
|
||||
- (instancetype)init;
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile;
|
||||
- (instancetype)initWithVnode:(SantaVnode)vnode NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@property SantaVnode vnodeId;
|
||||
@property SNTEventState decision;
|
||||
@@ -38,6 +40,7 @@
|
||||
@property NSArray<MOLCertificate *> *certChain;
|
||||
@property NSString *teamID;
|
||||
@property NSString *signingID;
|
||||
@property NSString *cdhash;
|
||||
@property NSDictionary *entitlements;
|
||||
@property BOOL entitlementsFiltered;
|
||||
|
||||
|
||||
@@ -17,10 +17,18 @@
|
||||
|
||||
@implementation SNTCachedDecision
|
||||
|
||||
- (instancetype)init {
|
||||
return [self initWithVnode:(SantaVnode){}];
|
||||
}
|
||||
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile {
|
||||
return [self initWithVnode:SantaVnode::VnodeForFile(esFile)];
|
||||
}
|
||||
|
||||
- (instancetype)initWithVnode:(SantaVnode)vnode {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_vnodeId = SantaVnode::VnodeForFile(esFile);
|
||||
_vnodeId = vnode;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ typedef NS_ENUM(NSInteger, SNTAction) {
|
||||
typedef NS_ENUM(NSInteger, SNTRuleType) {
|
||||
SNTRuleTypeUnknown = 0,
|
||||
|
||||
SNTRuleTypeCDHash = 500,
|
||||
SNTRuleTypeBinary = 1000,
|
||||
SNTRuleTypeSigningID = 2000,
|
||||
SNTRuleTypeCertificate = 3000,
|
||||
@@ -84,6 +85,7 @@ typedef NS_ENUM(uint64_t, SNTEventState) {
|
||||
SNTEventStateBlockTeamID = 1ULL << 20,
|
||||
SNTEventStateBlockLongPath = 1ULL << 21,
|
||||
SNTEventStateBlockSigningID = 1ULL << 22,
|
||||
SNTEventStateBlockCDHash = 1ULL << 23,
|
||||
|
||||
// Bits 40-63 store allow decision types
|
||||
SNTEventStateAllowUnknown = 1ULL << 40,
|
||||
@@ -95,6 +97,7 @@ typedef NS_ENUM(uint64_t, SNTEventState) {
|
||||
SNTEventStateAllowPendingTransitive = 1ULL << 46,
|
||||
SNTEventStateAllowTeamID = 1ULL << 47,
|
||||
SNTEventStateAllowSigningID = 1ULL << 48,
|
||||
SNTEventStateAllowCDHash = 1ULL << 49,
|
||||
|
||||
// Block and Allow masks
|
||||
SNTEventStateBlock = 0xFFFFFFULL << 16,
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
///
|
||||
/// Enable Fail Close mode. Defaults to NO.
|
||||
/// This controls Santa's behavior when a failure occurs, such as an
|
||||
/// inability to read a file. By default, to prevent bugs or misconfiguration
|
||||
/// inability to read a file and as a default response when deadlines
|
||||
/// are about to expire. By default, to prevent bugs or misconfiguration
|
||||
/// from rendering a machine inoperable Santa will fail open and allow
|
||||
/// execution. With this setting enabled, Santa will fail closed if the client
|
||||
/// is in LOCKDOWN mode, offering a higher level of security but with a higher
|
||||
@@ -654,6 +655,12 @@
|
||||
///
|
||||
@property(readonly, nonatomic) NSArray<NSString *> *entitlementsTeamIDFilter;
|
||||
|
||||
///
|
||||
/// List of enabled process annotations.
|
||||
/// This property is not KVO compliant.
|
||||
///
|
||||
@property(readonly, nonatomic) NSArray<NSString *> *enabledProcessAnnotations;
|
||||
|
||||
///
|
||||
/// Retrieve an initialized singleton configurator object using the default file path.
|
||||
///
|
||||
|
||||
@@ -147,6 +147,8 @@ static NSString *const kMetricExportInterval = @"MetricExportInterval";
|
||||
static NSString *const kMetricExportTimeout = @"MetricExportTimeout";
|
||||
static NSString *const kMetricExtraLabels = @"MetricExtraLabels";
|
||||
|
||||
static NSString *const kEnabledProcessAnnotations = @"EnabledProcessAnnotations";
|
||||
|
||||
// The keys managed by a sync server or mobileconfig.
|
||||
static NSString *const kClientModeKey = @"ClientMode";
|
||||
static NSString *const kBlockUSBMountKey = @"BlockUSBMount";
|
||||
@@ -275,6 +277,7 @@ static NSString *const kSyncTypeRequired = @"SyncTypeRequired";
|
||||
kOverrideFileAccessActionKey : string,
|
||||
kEntitlementsPrefixFilterKey : array,
|
||||
kEntitlementsTeamIDFilterKey : array,
|
||||
kEnabledProcessAnnotations : array,
|
||||
};
|
||||
|
||||
_syncStateFilePath = syncStateFilePath;
|
||||
@@ -604,8 +607,7 @@ static NSString *const kSyncTypeRequired = @"SyncTypeRequired";
|
||||
|
||||
- (BOOL)failClosed {
|
||||
NSNumber *n = self.configState[kFailClosedKey];
|
||||
if (n) return [n boolValue];
|
||||
return NO;
|
||||
return [n boolValue] && self.clientMode == SNTClientModeLockdown;
|
||||
}
|
||||
|
||||
- (BOOL)enableTransitiveRules {
|
||||
@@ -1106,6 +1108,16 @@ static NSString *const kSyncTypeRequired = @"SyncTypeRequired";
|
||||
return self.configState[kMetricExtraLabels];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)enabledProcessAnnotations {
|
||||
NSArray<NSString *> *annotations = self.configState[kEnabledProcessAnnotations];
|
||||
for (id annotation in annotations) {
|
||||
if (![annotation isKindOfClass:[NSString class]]) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
return annotations;
|
||||
}
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
///
|
||||
@@ -1210,6 +1222,39 @@ static NSString *const kSyncTypeRequired = @"SyncTypeRequired";
|
||||
return [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:NULL];
|
||||
}
|
||||
|
||||
- (void)applyOverrides:(NSMutableDictionary *)forcedConfig {
|
||||
// Overrides should only be applied under debug builds.
|
||||
#ifdef DEBUG
|
||||
if ([[[NSProcessInfo processInfo] processName] isEqualToString:@"xctest"] &&
|
||||
![[[NSProcessInfo processInfo] environment] objectForKey:@"ENABLE_CONFIG_OVERRIDES"]) {
|
||||
// By default, config overrides are not applied when running tests to help
|
||||
// mitigate potential issues due to unexpected config values. This behavior
|
||||
// can be overriden if desired by using the env variable: `ENABLE_CONFIG_OVERRIDES`.
|
||||
//
|
||||
// E.g.:
|
||||
// bazel test --test_env=ENABLE_CONFIG_OVERRIDES=1 ...other test args...
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary *overrides = [NSDictionary dictionaryWithContentsOfFile:kConfigOverrideFilePath];
|
||||
for (NSString *key in overrides) {
|
||||
id obj = overrides[key];
|
||||
if (![obj isKindOfClass:self.forcedConfigKeyTypes[key]] ||
|
||||
([self.forcedConfigKeyTypes[key] isKindOfClass:[NSRegularExpression class]] &&
|
||||
![obj isKindOfClass:[NSString class]])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
forcedConfig[key] = obj;
|
||||
|
||||
if (self.forcedConfigKeyTypes[key] == [NSRegularExpression class]) {
|
||||
NSString *pattern = [obj isKindOfClass:[NSString class]] ? obj : nil;
|
||||
forcedConfig[key] = [self expressionForPattern:pattern];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (NSMutableDictionary *)readForcedConfig {
|
||||
NSMutableDictionary *forcedConfig = [NSMutableDictionary dictionary];
|
||||
for (NSString *key in self.forcedConfigKeyTypes) {
|
||||
@@ -1221,22 +1266,9 @@ static NSString *const kSyncTypeRequired = @"SyncTypeRequired";
|
||||
forcedConfig[key] = [self expressionForPattern:pattern];
|
||||
}
|
||||
}
|
||||
#ifdef DEBUG
|
||||
NSDictionary *overrides = [NSDictionary dictionaryWithContentsOfFile:kConfigOverrideFilePath];
|
||||
for (NSString *key in overrides) {
|
||||
id obj = overrides[key];
|
||||
if (![obj isKindOfClass:self.forcedConfigKeyTypes[key]] ||
|
||||
([self.forcedConfigKeyTypes[key] isKindOfClass:[NSRegularExpression class]] &&
|
||||
![obj isKindOfClass:[NSString class]])) {
|
||||
continue;
|
||||
}
|
||||
forcedConfig[key] = obj;
|
||||
if (self.forcedConfigKeyTypes[key] == [NSRegularExpression class]) {
|
||||
NSString *pattern = [obj isKindOfClass:[NSString class]] ? obj : nil;
|
||||
forcedConfig[key] = [self expressionForPattern:pattern];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
[self applyOverrides:forcedConfig];
|
||||
|
||||
return forcedConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#import <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SantaVnode.h"
|
||||
|
||||
@class MOLCodesignChecker;
|
||||
|
||||
///
|
||||
@@ -220,6 +222,11 @@
|
||||
///
|
||||
- (NSUInteger)fileSize;
|
||||
|
||||
///
|
||||
/// @return The devno/ino pair of the file
|
||||
///
|
||||
- (SantaVnode)vnode;
|
||||
|
||||
///
|
||||
/// @return The underlying file handle.
|
||||
///
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
@property NSString *path;
|
||||
@property NSFileHandle *fileHandle;
|
||||
@property NSUInteger fileSize;
|
||||
@property SantaVnode vnode;
|
||||
@property NSString *fileOwnerHomeDir;
|
||||
@property NSString *sha256Storage;
|
||||
|
||||
@@ -110,6 +111,7 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
}
|
||||
|
||||
_fileSize = fileStat->st_size;
|
||||
_vnode = (SantaVnode){.fsid = fileStat->st_dev, .fileid = fileStat->st_ino};
|
||||
|
||||
if (_fileSize == 0) return nil;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#import "Source/common/SNTRule.h"
|
||||
|
||||
#include <CommonCrypto/CommonCrypto.h>
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#include <os/base.h>
|
||||
|
||||
#import "Source/common/SNTSyncConstants.h"
|
||||
@@ -103,6 +104,14 @@ static const NSUInteger kExpectedTeamIDLength = 10;
|
||||
break;
|
||||
}
|
||||
|
||||
case SNTRuleTypeCDHash: {
|
||||
identifier = [[identifier lowercaseString] stringByTrimmingCharactersInSet:nonHex];
|
||||
if (identifier.length != CS_CDHASH_LEN * 2) {
|
||||
return nil;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
@@ -173,6 +182,8 @@ static const NSUInteger kExpectedTeamIDLength = 10;
|
||||
type = SNTRuleTypeTeamID;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeSigningID]) {
|
||||
type = SNTRuleTypeSigningID;
|
||||
} else if ([ruleTypeString isEqual:kRuleTypeCDHash]) {
|
||||
type = SNTRuleTypeCDHash;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
51
Source/common/SNTRuleIdentifiers.h
Normal file
51
Source/common/SNTRuleIdentifiers.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/// Copyright 2024 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.
|
||||
|
||||
/**
|
||||
* This file declares two types that are mirrors of each other.
|
||||
*
|
||||
* The C struct serves as a way to group and pass valid rule identifiers around
|
||||
* in order to minimize interface changes needed when new rule types are added
|
||||
* and also alleviate the need to allocate a short lived object.
|
||||
*
|
||||
* The Objective C class is used for an XPC boundary to easily pass rule
|
||||
* identifiers between Santa components.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
struct RuleIdentifiers {
|
||||
NSString *cdhash;
|
||||
NSString *binarySHA256;
|
||||
NSString *signingID;
|
||||
NSString *certificateSHA256;
|
||||
NSString *teamID;
|
||||
};
|
||||
|
||||
@interface SNTRuleIdentifiers : NSObject <NSSecureCoding>
|
||||
@property(readonly) NSString *cdhash;
|
||||
@property(readonly) NSString *binarySHA256;
|
||||
@property(readonly) NSString *signingID;
|
||||
@property(readonly) NSString *certificateSHA256;
|
||||
@property(readonly) NSString *teamID;
|
||||
|
||||
/// Please use `initWithRuleIdentifiers:`
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (instancetype)initWithRuleIdentifiers:(struct RuleIdentifiers)identifiers
|
||||
NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (struct RuleIdentifiers)toStruct;
|
||||
|
||||
@end
|
||||
73
Source/common/SNTRuleIdentifiers.m
Normal file
73
Source/common/SNTRuleIdentifiers.m
Normal file
@@ -0,0 +1,73 @@
|
||||
/// Copyright 2024 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
|
||||
@implementation SNTRuleIdentifiers
|
||||
|
||||
- (instancetype)initWithRuleIdentifiers:(struct RuleIdentifiers)identifiers {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_cdhash = identifiers.cdhash;
|
||||
_binarySHA256 = identifiers.binarySHA256;
|
||||
_signingID = identifiers.signingID;
|
||||
_certificateSHA256 = identifiers.certificateSHA256;
|
||||
_teamID = identifiers.teamID;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (struct RuleIdentifiers)toStruct {
|
||||
return (struct RuleIdentifiers){.cdhash = self.cdhash,
|
||||
.binarySHA256 = self.binarySHA256,
|
||||
.signingID = self.signingID,
|
||||
.certificateSHA256 = self.certificateSHA256,
|
||||
.teamID = self.teamID};
|
||||
}
|
||||
|
||||
#pragma mark NSSecureCoding
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wobjc-literal-conversion"
|
||||
#define ENCODE(obj, key) \
|
||||
if (obj) [coder encodeObject:obj forKey:key]
|
||||
#define DECODE(cls, key) [decoder decodeObjectOfClass:[cls class] forKey:key]
|
||||
|
||||
+ (BOOL)supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)decoder {
|
||||
self = [self init];
|
||||
if (self) {
|
||||
_cdhash = DECODE(NSString, @"cdhash");
|
||||
_binarySHA256 = DECODE(NSString, @"binarySHA256");
|
||||
_signingID = DECODE(NSString, @"signingID");
|
||||
_certificateSHA256 = DECODE(NSString, @"certificateSHA256");
|
||||
_teamID = DECODE(NSString, @"teamID");
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder {
|
||||
ENCODE(self.cdhash, @"cdhash");
|
||||
ENCODE(self.binarySHA256, @"binarySHA256");
|
||||
ENCODE(self.signingID, @"signingID");
|
||||
ENCODE(self.certificateSHA256, @"certificateSHA256");
|
||||
ENCODE(self.teamID, @"teamID");
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@end
|
||||
@@ -105,6 +105,11 @@
|
||||
///
|
||||
@property NSString *signingID;
|
||||
|
||||
///
|
||||
/// If the executed file was signed, this is the CDHash of the binary.
|
||||
///
|
||||
@property NSString *cdhash;
|
||||
|
||||
///
|
||||
/// The user who executed the binary.
|
||||
///
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
ENCODE(self.signingChain, @"signingChain");
|
||||
ENCODE(self.teamID, @"teamID");
|
||||
ENCODE(self.signingID, @"signingID");
|
||||
ENCODE(self.cdhash, @"cdhash");
|
||||
|
||||
ENCODE(self.executingUser, @"executingUser");
|
||||
ENCODE(self.occurrenceDate, @"occurrenceDate");
|
||||
@@ -97,6 +98,7 @@
|
||||
_signingChain = DECODEARRAY(MOLCertificate, @"signingChain");
|
||||
_teamID = DECODE(NSString, @"teamID");
|
||||
_signingID = DECODE(NSString, @"signingID");
|
||||
_cdhash = DECODE(NSString, @"cdhash");
|
||||
|
||||
_executingUser = DECODE(NSString, @"executingUser");
|
||||
_occurrenceDate = DECODE(NSDate, @"occurrenceDate");
|
||||
|
||||
@@ -44,6 +44,7 @@ extern NSString *const kCompilerRuleCount;
|
||||
extern NSString *const kTransitiveRuleCount;
|
||||
extern NSString *const kTeamIDRuleCount;
|
||||
extern NSString *const kSigningIDRuleCount;
|
||||
extern NSString *const kCDHashRuleCount;
|
||||
extern NSString *const kFullSyncInterval;
|
||||
extern NSString *const kFCMToken;
|
||||
extern NSString *const kFCMFullSyncInterval;
|
||||
@@ -70,12 +71,14 @@ extern NSString *const kDecisionAllowCertificate;
|
||||
extern NSString *const kDecisionAllowScope;
|
||||
extern NSString *const kDecisionAllowTeamID;
|
||||
extern NSString *const kDecisionAllowSigningID;
|
||||
extern NSString *const kDecisionAllowCDHash;
|
||||
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 kDecisionBlockCDHash;
|
||||
extern NSString *const kDecisionUnknown;
|
||||
extern NSString *const kDecisionBundleBinary;
|
||||
extern NSString *const kLoggedInUsers;
|
||||
@@ -101,6 +104,7 @@ extern NSString *const kCertValidFrom;
|
||||
extern NSString *const kCertValidUntil;
|
||||
extern NSString *const kTeamID;
|
||||
extern NSString *const kSigningID;
|
||||
extern NSString *const kCDHash;
|
||||
extern NSString *const kQuarantineDataURL;
|
||||
extern NSString *const kQuarantineRefererURL;
|
||||
extern NSString *const kQuarantineTimestamp;
|
||||
@@ -125,6 +129,7 @@ extern NSString *const kRuleTypeBinary;
|
||||
extern NSString *const kRuleTypeCertificate;
|
||||
extern NSString *const kRuleTypeTeamID;
|
||||
extern NSString *const kRuleTypeSigningID;
|
||||
extern NSString *const kRuleTypeCDHash;
|
||||
extern NSString *const kRuleCustomMsg;
|
||||
extern NSString *const kRuleCustomURL;
|
||||
extern NSString *const kCursor;
|
||||
|
||||
@@ -44,6 +44,7 @@ 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 kCDHashRuleCount = @"cdhash_rule_count";
|
||||
NSString *const kFullSyncInterval = @"full_sync_interval";
|
||||
NSString *const kFCMToken = @"fcm_token";
|
||||
NSString *const kFCMFullSyncInterval = @"fcm_full_sync_interval";
|
||||
@@ -71,12 +72,14 @@ NSString *const kDecisionAllowCertificate = @"ALLOW_CERTIFICATE";
|
||||
NSString *const kDecisionAllowScope = @"ALLOW_SCOPE";
|
||||
NSString *const kDecisionAllowTeamID = @"ALLOW_TEAMID";
|
||||
NSString *const kDecisionAllowSigningID = @"ALLOW_SIGNINGID";
|
||||
NSString *const kDecisionAllowCDHash = @"ALLOW_CDHASH";
|
||||
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 kDecisionBlockCDHash = @"BLOCK_CDHASH";
|
||||
NSString *const kDecisionUnknown = @"UNKNOWN";
|
||||
NSString *const kDecisionBundleBinary = @"BUNDLE_BINARY";
|
||||
NSString *const kLoggedInUsers = @"logged_in_users";
|
||||
@@ -102,6 +105,7 @@ NSString *const kCertValidFrom = @"valid_from";
|
||||
NSString *const kCertValidUntil = @"valid_until";
|
||||
NSString *const kTeamID = @"team_id";
|
||||
NSString *const kSigningID = @"signing_id";
|
||||
NSString *const kCDHash = @"cdhash";
|
||||
NSString *const kQuarantineDataURL = @"quarantine_data_url";
|
||||
NSString *const kQuarantineRefererURL = @"quarantine_referer_url";
|
||||
NSString *const kQuarantineTimestamp = @"quarantine_timestamp";
|
||||
@@ -126,6 +130,7 @@ NSString *const kRuleTypeBinary = @"BINARY";
|
||||
NSString *const kRuleTypeCertificate = @"CERTIFICATE";
|
||||
NSString *const kRuleTypeTeamID = @"TEAMID";
|
||||
NSString *const kRuleTypeSigningID = @"SIGNINGID";
|
||||
NSString *const kRuleTypeCDHash = @"CDHASH";
|
||||
NSString *const kRuleCustomMsg = @"custom_msg";
|
||||
NSString *const kRuleCustomURL = @"custom_url";
|
||||
NSString *const kCursor = @"cursor";
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
#import "Source/common/SNTXPCUnprivilegedControlInterface.h"
|
||||
|
||||
///
|
||||
@@ -32,11 +33,8 @@
|
||||
reply:(void (^)(NSError *error))reply;
|
||||
- (void)databaseEventsPending:(void (^)(NSArray *events))reply;
|
||||
- (void)databaseRemoveEventsWithIDs:(NSArray *)ids;
|
||||
- (void)databaseRuleForBinarySHA256:(NSString *)binarySHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
reply:(void (^)(SNTRule *))reply;
|
||||
- (void)databaseRuleForIdentifiers:(SNTRuleIdentifiers *)identifiers
|
||||
reply:(void (^)(SNTRule *))reply;
|
||||
- (void)retrieveAllRules:(void (^)(NSArray<SNTRule *> *rules, NSError *error))reply;
|
||||
|
||||
///
|
||||
|
||||
@@ -16,12 +16,23 @@
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
|
||||
@class SNTRule;
|
||||
@class SNTStoredEvent;
|
||||
@class MOLXPCConnection;
|
||||
|
||||
struct RuleCounts {
|
||||
int64_t binary;
|
||||
int64_t certificate;
|
||||
int64_t compiler;
|
||||
int64_t transitive;
|
||||
int64_t teamID;
|
||||
int64_t signingID;
|
||||
int64_t cdhash;
|
||||
};
|
||||
|
||||
///
|
||||
/// Protocol implemented by santad and utilized by santactl (unprivileged operations)
|
||||
///
|
||||
@@ -36,8 +47,7 @@
|
||||
///
|
||||
/// Database ops
|
||||
///
|
||||
- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive, int64_t teamID, int64_t signingID))reply;
|
||||
- (void)databaseRuleCounts:(void (^)(struct RuleCounts ruleCounts))reply;
|
||||
- (void)databaseEventCount:(void (^)(int64_t count))reply;
|
||||
- (void)staticRuleCount:(void (^)(int64_t count))reply;
|
||||
|
||||
@@ -47,17 +57,10 @@
|
||||
|
||||
///
|
||||
/// @param filePath A Path to the file, can be nil.
|
||||
/// @param fileSHA256 The pre-calculated SHA256 hash for the file, can be nil. If nil the hash will
|
||||
/// be calculated by this method from the filePath.
|
||||
/// @param certificateSHA256 A SHA256 hash of the signing certificate, can be nil.
|
||||
/// @note If fileInfo and signingCertificate are both passed in, the most specific rule will be
|
||||
/// returned. Binary rules take precedence over cert rules.
|
||||
/// @param identifiers The various identifiers to be used when making a decision.
|
||||
///
|
||||
- (void)decisionForFilePath:(NSString *)filePath
|
||||
fileSHA256:(NSString *)fileSHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
identifiers:(SNTRuleIdentifiers *)identifiers
|
||||
reply:(void (^)(SNTEventState))reply;
|
||||
|
||||
///
|
||||
|
||||
@@ -320,8 +320,8 @@ class SantaCache {
|
||||
Lock a bucket. Spins until the lock is acquired.
|
||||
*/
|
||||
inline void lock(struct bucket *bucket) const {
|
||||
while (OSAtomicTestAndSet(7, (volatile uint8_t *)&bucket->head))
|
||||
;
|
||||
while (OSAtomicTestAndSet(7, (volatile uint8_t *)&bucket->head)) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ syntax = "proto3";
|
||||
|
||||
import "google/protobuf/any.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "Source/santad/ProcessTree/process_tree.proto";
|
||||
|
||||
option objc_class_prefix = "SNTPB";
|
||||
|
||||
@@ -173,6 +174,8 @@ message ProcessInfo {
|
||||
|
||||
// Time the process was started
|
||||
optional google.protobuf.Timestamp start_time = 17;
|
||||
|
||||
optional process_tree.Annotations annotations = 18;
|
||||
}
|
||||
|
||||
// Light variant of ProcessInfo message to help minimize on-disk/on-wire sizes
|
||||
@@ -202,6 +205,8 @@ message ProcessInfoLight {
|
||||
|
||||
// File information for the executable backing this process
|
||||
optional FileInfoLight executable = 10;
|
||||
|
||||
optional process_tree.Annotations annotations = 11;
|
||||
}
|
||||
|
||||
// Certificate information
|
||||
@@ -284,6 +289,7 @@ message Execution {
|
||||
REASON_LONG_PATH = 9;
|
||||
REASON_NOT_RUNNING = 10;
|
||||
REASON_SIGNING_ID = 11;
|
||||
REASON_CDHASH = 12;
|
||||
}
|
||||
optional Reason reason = 10;
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ objc_library(
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTRuleIdentifiers",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SNTStrengthify",
|
||||
"//Source/common:SNTSystemInfo",
|
||||
@@ -116,6 +117,7 @@ santa_unit_test(
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTRuleIdentifiers",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SNTXPCBundleServiceInterface",
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/common/SNTXPCBundleServiceInterface.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
@@ -45,6 +46,7 @@ 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";
|
||||
static NSString *const kCDHash = @"CDHash";
|
||||
|
||||
// signing chain keys
|
||||
static NSString *const kCommonName = @"Common Name";
|
||||
@@ -123,6 +125,7 @@ typedef id (^SNTAttributeBlock)(SNTCommandFileInfo *, SNTFileInfo *);
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock downloadAgent;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock teamID;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock signingID;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock cdhash;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock type;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock pageZero;
|
||||
@property(readonly, copy, nonatomic) SNTAttributeBlock codeSigned;
|
||||
@@ -201,8 +204,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
+ (NSArray<NSString *> *)fileInfoKeys {
|
||||
return @[
|
||||
kPath, kSHA256, kSHA1, kBundleName, kBundleVersion, kBundleVersionStr, kDownloadReferrerURL,
|
||||
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kSigningID, kType, kPageZero,
|
||||
kCodeSigned, kRule, kSigningChain, kUniversalSigningChain
|
||||
kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kSigningID, kCDHash, kType,
|
||||
kPageZero, kCodeSigned, kRule, kSigningChain, kUniversalSigningChain
|
||||
];
|
||||
}
|
||||
|
||||
@@ -236,6 +239,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
kUniversalSigningChain : self.universalSigningChain,
|
||||
kTeamID : self.teamID,
|
||||
kSigningID : self.signingID,
|
||||
kCDHash : self.cdhash,
|
||||
};
|
||||
|
||||
_printQueue = dispatch_queue_create("com.google.santactl.print_queue", DISPATCH_QUEUE_SERIAL);
|
||||
@@ -376,6 +380,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
NSError *err;
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:&err];
|
||||
|
||||
NSString *cdhash =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoUnique];
|
||||
NSString *teamID =
|
||||
[csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier];
|
||||
NSString *identifier =
|
||||
@@ -394,15 +400,21 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
}
|
||||
}
|
||||
|
||||
[[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);
|
||||
}];
|
||||
struct RuleIdentifiers identifiers = {
|
||||
.cdhash = cdhash,
|
||||
.binarySHA256 = fileInfo.SHA256,
|
||||
.signingID = signingID,
|
||||
.certificateSHA256 = err ? nil : csc.leafCertificate.SHA256,
|
||||
.teamID = teamID,
|
||||
};
|
||||
|
||||
[[cmd.daemonConn remoteObjectProxy]
|
||||
decisionForFilePath:fileInfo.path
|
||||
identifiers:[[SNTRuleIdentifiers alloc] initWithRuleIdentifiers:identifiers]
|
||||
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;
|
||||
@@ -420,6 +432,8 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
|
||||
case SNTEventStateAllowSigningID:
|
||||
case SNTEventStateBlockSigningID: [output appendString:@" (SigningID)"]; break;
|
||||
case SNTEventStateAllowCDHash:
|
||||
case SNTEventStateBlockCDHash: [output appendString:@" (CDHash)"]; break;
|
||||
case SNTEventStateAllowScope:
|
||||
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
|
||||
case SNTEventStateAllowCompiler: [output appendString:@" (Compiler)"]; break;
|
||||
@@ -519,6 +533,13 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
};
|
||||
}
|
||||
|
||||
- (SNTAttributeBlock)cdhash {
|
||||
return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) {
|
||||
MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL];
|
||||
return [csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoUnique];
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
// Entry point for the command.
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <CommonCrypto/CommonDigest.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Kernel/kern/cs_blobs.h>
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
@@ -22,6 +24,7 @@
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/santactl/Commands/SNTCommandRule.h"
|
||||
#import "Source/santactl/SNTCommand.h"
|
||||
@@ -44,58 +47,66 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
}
|
||||
|
||||
+ (NSString *)longHelpText {
|
||||
return (@"Usage: santactl rule [options]\n"
|
||||
@" One of:\n"
|
||||
@" --allow: add to allow\n"
|
||||
@" --block: add to block\n"
|
||||
@" --silent-block: add to silent block\n"
|
||||
@" --compiler: allow and mark as a compiler\n"
|
||||
@" --remove: remove existing rule\n"
|
||||
@" --check: check for an existing rule\n"
|
||||
@" --import {path}: import rules from a JSON file\n"
|
||||
@" --export {path}: export rules to a JSON file\n"
|
||||
@"\n"
|
||||
@" One of:\n"
|
||||
@" --path {path}: path of binary/bundle to add/remove.\n"
|
||||
@" 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|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"
|
||||
return (
|
||||
@"Usage: santactl rule [options]\n"
|
||||
@" One of:\n"
|
||||
@" --allow: add to allow\n"
|
||||
@" --block: add to block\n"
|
||||
@" --silent-block: add to silent block\n"
|
||||
@" --compiler: allow and mark as a compiler\n"
|
||||
@" --remove: remove existing rule\n"
|
||||
@" --check: check for an existing rule\n"
|
||||
@" --import {path}: import rules from a JSON file\n"
|
||||
@" --export {path}: export rules to a JSON file\n"
|
||||
@"\n"
|
||||
@" One of:\n"
|
||||
@" --path {path}: path of binary/bundle to add/remove.\n"
|
||||
@" 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|signingID|cdhash}: 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"
|
||||
@" --cdhash: add or check a cdhash rule instead of binary\n"
|
||||
#ifdef DEBUG
|
||||
@" --force: allow manual changes even when SyncBaseUrl is set\n"
|
||||
@" --force: allow manual changes even when SyncBaseUrl is set\n"
|
||||
#endif
|
||||
@" --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"
|
||||
@"\n"
|
||||
@" Importing / Exporting Rules:\n"
|
||||
@" If santa is not configured to use a sync server one can export\n"
|
||||
@" & import its non-static rules to and from JSON files using the \n"
|
||||
@" --export/--import flags. These files have the following form:\n"
|
||||
@"\n"
|
||||
@" {\"rules\": [{rule-dictionaries}]}\n"
|
||||
@" e.g. {\"rules\": [\n"
|
||||
@" {\"policy\": \"BLOCKLIST\",\n"
|
||||
@" \"identifier\": "
|
||||
@"\"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6\"\n"
|
||||
@" \"custom_url\" : \"\",\n"
|
||||
@" \"custom_msg\": \"/bin/ls block for demo\"}\n"
|
||||
@" ]}\n");
|
||||
@" --message {message}: custom message\n"
|
||||
@" --clean: when importing rules via JSON clear all non-transitive rules before importing\n"
|
||||
@" --clean-all: when importing rules via JSON clear all rules before importing\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"
|
||||
@"\n"
|
||||
@" Importing / Exporting Rules:\n"
|
||||
@" If santa is not configured to use a sync server one can export\n"
|
||||
@" & import its non-static rules to and from JSON files using the \n"
|
||||
@" --export/--import flags. These files have the following form:\n"
|
||||
@"\n"
|
||||
@" {\"rules\": [{rule-dictionaries}]}\n"
|
||||
@" e.g. {\"rules\": [\n"
|
||||
@" {\"policy\": \"BLOCKLIST\",\n"
|
||||
@" \"identifier\": "
|
||||
@"\"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6\"\n"
|
||||
@" \"custom_url\" : \"\",\n"
|
||||
@" \"custom_msg\": \"/bin/ls block for demo\"}\n"
|
||||
@" ]}\n"
|
||||
@"\n"
|
||||
@" By default rules are not cleared when importing. To clear the\n"
|
||||
@" database you must use either --clean or --clean-all\n"
|
||||
@"\n");
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
@@ -119,6 +130,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
NSString *path;
|
||||
NSString *jsonFilePath;
|
||||
BOOL check = NO;
|
||||
SNTRuleCleanup cleanupType = SNTRuleCleanupNone;
|
||||
BOOL importRules = NO;
|
||||
BOOL exportRules = NO;
|
||||
|
||||
@@ -147,6 +159,8 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
newRule.type = SNTRuleTypeTeamID;
|
||||
} else if ([arg caseInsensitiveCompare:@"--signingid"] == NSOrderedSame) {
|
||||
newRule.type = SNTRuleTypeSigningID;
|
||||
} else if ([arg caseInsensitiveCompare:@"--cdhash"] == NSOrderedSame) {
|
||||
newRule.type = SNTRuleTypeCDHash;
|
||||
} else if ([arg caseInsensitiveCompare:@"--path"] == NSOrderedSame) {
|
||||
if (++i > arguments.count - 1) {
|
||||
[self printErrorUsageAndExit:@"--path requires an argument"];
|
||||
@@ -180,6 +194,10 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
[self printErrorUsageAndExit:@"--import requires an argument"];
|
||||
}
|
||||
jsonFilePath = arguments[i];
|
||||
} else if ([arg caseInsensitiveCompare:@"--clean"] == NSOrderedSame) {
|
||||
cleanupType = SNTRuleCleanupNonTransitive;
|
||||
} else if ([arg caseInsensitiveCompare:@"--clean-all"] == NSOrderedSame) {
|
||||
cleanupType = SNTRuleCleanupAll;
|
||||
} else if ([arg caseInsensitiveCompare:@"--export"] == NSOrderedSame) {
|
||||
if (importRules) {
|
||||
[self printErrorUsageAndExit:@"--import and --export are mutually exclusive"];
|
||||
@@ -198,12 +216,27 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
}
|
||||
}
|
||||
|
||||
if (!importRules && cleanupType != SNTRuleCleanupNone) {
|
||||
switch (cleanupType) {
|
||||
case SNTRuleCleanupNonTransitive:
|
||||
[self printErrorUsageAndExit:@"--clean can only be used with --import"];
|
||||
break;
|
||||
case SNTRuleCleanupAll:
|
||||
[self printErrorUsageAndExit:@"--clean-all can only be used with --import"];
|
||||
break;
|
||||
default:
|
||||
// This is a programming error.
|
||||
LOGE(@"Unexpected SNTRuleCleanupType %ld", cleanupType);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonFilePath.length > 0) {
|
||||
if (importRules) {
|
||||
if (newRule.identifier != nil || path != nil || check) {
|
||||
[self printErrorUsageAndExit:@"--import can only be used by itself"];
|
||||
}
|
||||
[self importJSONFile:jsonFilePath];
|
||||
[self importJSONFile:jsonFilePath with:cleanupType];
|
||||
} else if (exportRules) {
|
||||
if (newRule.identifier != nil || path != nil || check) {
|
||||
[self printErrorUsageAndExit:@"--export can only be used by itself"];
|
||||
@@ -224,17 +257,29 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
} else if (newRule.type == SNTRuleTypeCertificate) {
|
||||
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
|
||||
newRule.identifier = cs.leafCertificate.SHA256;
|
||||
} else if (newRule.type == SNTRuleTypeCDHash) {
|
||||
MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL];
|
||||
newRule.identifier =
|
||||
[cs.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier];
|
||||
} else if (newRule.type == SNTRuleTypeTeamID || newRule.type == SNTRuleTypeSigningID) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
if (newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate) {
|
||||
if (newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate ||
|
||||
newRule.type == SNTRuleTypeCDHash) {
|
||||
NSCharacterSet *nonHex =
|
||||
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
|
||||
if ([[newRule.identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length !=
|
||||
64) {
|
||||
NSUInteger length =
|
||||
[[newRule.identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length;
|
||||
|
||||
if ((newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate) &&
|
||||
length != CC_SHA256_DIGEST_LENGTH * 2) {
|
||||
[self printErrorUsageAndExit:@"BINARY or CERTIFICATE rules require a valid SHA-256"];
|
||||
} else if (newRule.type == SNTRuleTypeCDHash && length != CS_CDHASH_LEN * 2) {
|
||||
[self printErrorUsageAndExit:
|
||||
[NSString stringWithFormat:@"CDHASH rules require a valid hex string of length %d",
|
||||
CS_CDHASH_LEN * 2]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +306,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
} else {
|
||||
NSString *ruleType;
|
||||
switch (newRule.type) {
|
||||
case SNTRuleTypeCertificate:
|
||||
case SNTRuleTypeCertificate: ruleType = @"Certificate SHA-256"; break;
|
||||
case SNTRuleTypeBinary: {
|
||||
ruleType = @"SHA-256";
|
||||
break;
|
||||
@@ -270,6 +315,14 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
ruleType = @"Team ID";
|
||||
break;
|
||||
}
|
||||
case SNTRuleTypeSigningID: {
|
||||
ruleType = @"Signing ID";
|
||||
break;
|
||||
}
|
||||
case SNTRuleTypeCDHash: {
|
||||
ruleType = @"CDHash";
|
||||
break;
|
||||
}
|
||||
default: ruleType = @"(Unknown type)";
|
||||
}
|
||||
if (newRule.state == SNTRuleStateRemove) {
|
||||
@@ -323,6 +376,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
|
||||
switch (rule.type) {
|
||||
case SNTRuleTypeUnknown: [output appendString:@"Unknown"]; break;
|
||||
case SNTRuleTypeCDHash: [output appendString:@"CDHash"]; break;
|
||||
case SNTRuleTypeBinary: [output appendString:@"Binary"]; break;
|
||||
case SNTRuleTypeSigningID: [output appendString:@"SigningID"]; break;
|
||||
case SNTRuleTypeCertificate: [output appendString:@"Certificate"]; break;
|
||||
@@ -365,26 +419,27 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
|
||||
- (void)printStateOfRule:(SNTRule *)rule daemonConnection:(MOLXPCConnection *)daemonConn {
|
||||
id<SNTDaemonControlXPC> rop = [daemonConn synchronousRemoteObjectProxy];
|
||||
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 NSString *output;
|
||||
|
||||
[rop databaseRuleForBinarySHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
reply:^(SNTRule *r) {
|
||||
output = [SNTCommandRule stringifyRule:r
|
||||
withColor:(isatty(STDOUT_FILENO) == 1)];
|
||||
}];
|
||||
struct RuleIdentifiers identifiers = {
|
||||
.cdhash = (rule.type == SNTRuleTypeCDHash) ? rule.identifier : nil,
|
||||
.binarySHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil,
|
||||
.certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil,
|
||||
.teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil,
|
||||
.signingID = (rule.type == SNTRuleTypeSigningID) ? rule.identifier : nil,
|
||||
};
|
||||
|
||||
[rop databaseRuleForIdentifiers:[[SNTRuleIdentifiers alloc] initWithRuleIdentifiers:identifiers]
|
||||
reply:^(SNTRule *r) {
|
||||
output = [SNTCommandRule stringifyRule:r
|
||||
withColor:(isatty(STDOUT_FILENO) == 1)];
|
||||
}];
|
||||
|
||||
printf("%s\n", output.UTF8String);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
- (void)importJSONFile:(NSString *)jsonFilePath {
|
||||
- (void)importJSONFile:(NSString *)jsonFilePath with:(SNTRuleCleanup)cleanupType {
|
||||
// If the file exists parse it and then add the rules one at a time.
|
||||
NSError *error;
|
||||
NSData *data = [NSData dataWithContentsOfFile:jsonFilePath options:0 error:&error];
|
||||
@@ -421,7 +476,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
databaseRuleAddRules:parsedRules
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
ruleCleanup:cleanupType
|
||||
reply:^(NSError *error) {
|
||||
if (error) {
|
||||
printf("Failed to modify rules: %s",
|
||||
|
||||
@@ -91,22 +91,20 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
}];
|
||||
|
||||
// Database counts
|
||||
__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 signingID) {
|
||||
binaryRuleCount = binary;
|
||||
certRuleCount = certificate;
|
||||
teamIDRuleCount = teamID;
|
||||
signingIDRuleCount = signingID;
|
||||
compilerRuleCount = compiler;
|
||||
transitiveRuleCount = transitive;
|
||||
__block struct RuleCounts ruleCounts = {
|
||||
.binary = -1,
|
||||
.certificate = -1,
|
||||
.compiler = -1,
|
||||
.transitive = -1,
|
||||
.teamID = -1,
|
||||
.signingID = -1,
|
||||
.cdhash = -1,
|
||||
};
|
||||
[rop databaseRuleCounts:^(struct RuleCounts counts) {
|
||||
ruleCounts = counts;
|
||||
}];
|
||||
|
||||
__block int64_t eventCount = -1;
|
||||
[rop databaseEventCount:^(int64_t count) {
|
||||
eventCount = count;
|
||||
}];
|
||||
@@ -212,12 +210,13 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"on_start_usb_options" : StartupOptionToString(configurator.onStartUSBOptions),
|
||||
},
|
||||
@"database" : @{
|
||||
@"binary_rules" : @(binaryRuleCount),
|
||||
@"certificate_rules" : @(certRuleCount),
|
||||
@"teamid_rules" : @(teamIDRuleCount),
|
||||
@"signingid_rules" : @(signingIDRuleCount),
|
||||
@"compiler_rules" : @(compilerRuleCount),
|
||||
@"transitive_rules" : @(transitiveRuleCount),
|
||||
@"binary_rules" : @(ruleCounts.binary),
|
||||
@"certificate_rules" : @(ruleCounts.certificate),
|
||||
@"teamid_rules" : @(ruleCounts.teamID),
|
||||
@"signingid_rules" : @(ruleCounts.signingID),
|
||||
@"cdhash_rules" : @(ruleCounts.cdhash),
|
||||
@"compiler_rules" : @(ruleCounts.compiler),
|
||||
@"transitive_rules" : @(ruleCounts.transitive),
|
||||
@"events_pending_upload" : @(eventCount),
|
||||
},
|
||||
@"static_rules" : @{
|
||||
@@ -284,12 +283,13 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
printf(" %-25s | %lld\n", "Non-root cache count", nonRootCacheCount);
|
||||
|
||||
printf(">>> Database Info\n");
|
||||
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", "Binary Rules", ruleCounts.binary);
|
||||
printf(" %-25s | %lld\n", "Certificate Rules", ruleCounts.certificate);
|
||||
printf(" %-25s | %lld\n", "TeamID Rules", ruleCounts.teamID);
|
||||
printf(" %-25s | %lld\n", "SigningID Rules", ruleCounts.signingID);
|
||||
printf(" %-25s | %lld\n", "CDHash Rules", ruleCounts.cdhash);
|
||||
printf(" %-25s | %lld\n", "Compiler Rules", ruleCounts.compiler);
|
||||
printf(" %-25s | %lld\n", "Transitive Rules", ruleCounts.transitive);
|
||||
printf(" %-25s | %lld\n", "Events Pending Upload", eventCount);
|
||||
|
||||
if ([SNTConfigurator configurator].staticRules.count) {
|
||||
|
||||
@@ -33,6 +33,7 @@ objc_library(
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTRuleIdentifiers",
|
||||
"@MOLCertificate",
|
||||
"@MOLCodesignChecker",
|
||||
],
|
||||
@@ -192,13 +193,10 @@ objc_library(
|
||||
|
||||
objc_library(
|
||||
name = "SNTPolicyProcessor",
|
||||
srcs = [
|
||||
"DataLayer/SNTDatabaseTable.h",
|
||||
"DataLayer/SNTRuleTable.h",
|
||||
"SNTPolicyProcessor.m",
|
||||
],
|
||||
srcs = ["SNTPolicyProcessor.m"],
|
||||
hdrs = ["SNTPolicyProcessor.h"],
|
||||
deps = [
|
||||
":SNTRuleTable",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
@@ -206,6 +204,7 @@ objc_library(
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTRuleIdentifiers",
|
||||
"@FMDB",
|
||||
"@MOLCertificate",
|
||||
"@MOLCodesignChecker",
|
||||
@@ -286,12 +285,27 @@ objc_library(
|
||||
":SNTEndpointSecurityClientBase",
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:BranchPrediction",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SystemResources",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTEndpointSecurityTreeAwareClient",
|
||||
srcs = ["EventProviders/SNTEndpointSecurityTreeAwareClient.mm"],
|
||||
hdrs = ["EventProviders/SNTEndpointSecurityTreeAwareClient.h"],
|
||||
deps = [
|
||||
":EndpointSecurityAPI",
|
||||
":EndpointSecurityMessage",
|
||||
":Metrics",
|
||||
":SNTEndpointSecurityClient",
|
||||
"//Source/santad/ProcessTree:SNTEndpointSecurityAdapter",
|
||||
"//Source/santad/ProcessTree:process_tree",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTEndpointSecurityRecorder",
|
||||
srcs = ["EventProviders/SNTEndpointSecurityRecorder.mm"],
|
||||
@@ -305,13 +319,14 @@ objc_library(
|
||||
":EndpointSecurityMessage",
|
||||
":Metrics",
|
||||
":SNTCompilerController",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
":SNTEndpointSecurityTreeAwareClient",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:String",
|
||||
"//Source/common:Unit",
|
||||
"//Source/santad/ProcessTree:process_tree",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -445,6 +460,8 @@ objc_library(
|
||||
":EndpointSecurityEnrichedTypes",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/santad/ProcessTree:SNTEndpointSecurityAdapter",
|
||||
"//Source/santad/ProcessTree:process_tree",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -453,6 +470,7 @@ objc_library(
|
||||
hdrs = ["EventProviders/EndpointSecurity/EnrichedTypes.h"],
|
||||
deps = [
|
||||
":EndpointSecurityMessage",
|
||||
"//Source/santad/ProcessTree:process_tree_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -627,6 +645,7 @@ objc_library(
|
||||
deps = [
|
||||
":EndpointSecurityClient",
|
||||
":WatchItemPolicy",
|
||||
"//Source/santad/ProcessTree:process_tree",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -639,6 +658,9 @@ objc_library(
|
||||
name = "EndpointSecurityAPI",
|
||||
srcs = ["EventProviders/EndpointSecurity/EndpointSecurityAPI.mm"],
|
||||
hdrs = ["EventProviders/EndpointSecurity/EndpointSecurityAPI.h"],
|
||||
sdk_dylibs = [
|
||||
"EndpointSecurity",
|
||||
],
|
||||
deps = [
|
||||
":EndpointSecurityClient",
|
||||
":EndpointSecurityMessage",
|
||||
@@ -667,6 +689,7 @@ objc_library(
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTRuleIdentifiers",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SNTStrengthify",
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
@@ -723,6 +746,7 @@ objc_library(
|
||||
"//Source/common:SNTXPCNotifierInterface",
|
||||
"//Source/common:SNTXPCSyncServiceInterface",
|
||||
"//Source/common:Unit",
|
||||
"//Source/santad/ProcessTree:process_tree",
|
||||
"@MOLXPCConnection",
|
||||
],
|
||||
)
|
||||
@@ -754,6 +778,7 @@ objc_library(
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
"//Source/common:SNTXPCUnprivilegedControlInterface",
|
||||
"//Source/common:Unit",
|
||||
"//Source/santad/ProcessTree:process_tree",
|
||||
"@MOLXPCConnection",
|
||||
],
|
||||
)
|
||||
@@ -876,9 +901,11 @@ santa_unit_test(
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTRuleIdentifiers",
|
||||
"@FMDB",
|
||||
"@MOLCertificate",
|
||||
"@MOLCodesignChecker",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -904,6 +931,7 @@ santa_unit_test(
|
||||
":SNTDatabaseController",
|
||||
":SNTDecisionCache",
|
||||
":SNTEndpointSecurityAuthorizer",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SantadDeps",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTConfigurator",
|
||||
@@ -1161,6 +1189,7 @@ santa_unit_test(
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
],
|
||||
@@ -1181,7 +1210,9 @@ santa_unit_test(
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTEndpointSecurityClient",
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SystemResources",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
"@com_google_googletest//:gtest",
|
||||
@@ -1209,6 +1240,7 @@ santa_unit_test(
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SNTRuleIdentifiers",
|
||||
"//Source/common:TestUtils",
|
||||
"@MOLCertificate",
|
||||
"@MOLCodesignChecker",
|
||||
@@ -1325,6 +1357,7 @@ santa_unit_test(
|
||||
":EndpointSecurityMessage",
|
||||
":Metrics",
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityDeviceManager",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
#import "Source/santad/DataLayer/SNTDatabaseTable.h"
|
||||
|
||||
@class SNTCachedDecision;
|
||||
@@ -29,46 +30,49 @@
|
||||
///
|
||||
/// @return Number of rules in the database
|
||||
///
|
||||
- (NSUInteger)ruleCount;
|
||||
- (int64_t)ruleCount;
|
||||
|
||||
///
|
||||
/// @return Number of binary rules in the database
|
||||
///
|
||||
- (NSUInteger)binaryRuleCount;
|
||||
- (int64_t)binaryRuleCount;
|
||||
|
||||
///
|
||||
/// @return Number of compiler rules in the database
|
||||
///
|
||||
- (NSUInteger)compilerRuleCount;
|
||||
- (int64_t)compilerRuleCount;
|
||||
|
||||
///
|
||||
/// @return Number of transitive rules in the database
|
||||
///
|
||||
- (NSUInteger)transitiveRuleCount;
|
||||
- (int64_t)transitiveRuleCount;
|
||||
|
||||
///
|
||||
/// @return Number of certificate rules in the database
|
||||
///
|
||||
- (NSUInteger)certificateRuleCount;
|
||||
- (int64_t)certificateRuleCount;
|
||||
|
||||
///
|
||||
/// @return Number of team ID rules in the database
|
||||
///
|
||||
- (NSUInteger)teamIDRuleCount;
|
||||
- (int64_t)teamIDRuleCount;
|
||||
|
||||
///
|
||||
/// @return Number of signing ID rules in the database
|
||||
///
|
||||
- (NSUInteger)signingIDRuleCount;
|
||||
- (int64_t)signingIDRuleCount;
|
||||
|
||||
///
|
||||
/// @return Rule for binary, signingID, certificate or teamID (in that order).
|
||||
/// @return Number of cdhash rules in the database
|
||||
///
|
||||
- (int64_t)cdhashRuleCount;
|
||||
|
||||
///
|
||||
/// @return Rule for given identifiers.
|
||||
/// Currently: 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;
|
||||
- (SNTRule *)ruleForIdentifiers:(struct RuleIdentifiers)identifiers;
|
||||
|
||||
///
|
||||
/// Add an array of rules to the database. The rules will be added within a transaction and the
|
||||
|
||||
@@ -29,7 +29,7 @@ static const uint32_t kRuleTableCurrentVersion = 7;
|
||||
|
||||
// TODO(nguyenphillip): this should be configurable.
|
||||
// How many rules must be in database before we start trying to remove transitive rules.
|
||||
static const NSUInteger kTransitiveRuleCullingThreshold = 500000;
|
||||
static const int64_t kTransitiveRuleCullingThreshold = 500000;
|
||||
// Consider transitive rules out of date if they haven't been used in six months.
|
||||
static const NSUInteger kTransitiveRuleExpirationSeconds = 6 * 30 * 24 * 3600;
|
||||
|
||||
@@ -263,7 +263,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
|
||||
#pragma mark Entry Counts
|
||||
|
||||
- (NSUInteger)ruleCount {
|
||||
- (int64_t)ruleCount {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules"];
|
||||
@@ -271,23 +271,23 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSUInteger)ruleCountForRuleType:(SNTRuleType)ruleType {
|
||||
__block NSUInteger count = 0;
|
||||
- (int64_t)ruleCountForRuleType:(SNTRuleType)ruleType {
|
||||
__block int64_t count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE type=?", @(ruleType)];
|
||||
}];
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSUInteger)binaryRuleCount {
|
||||
- (int64_t)binaryRuleCount {
|
||||
return [self ruleCountForRuleType:SNTRuleTypeBinary];
|
||||
}
|
||||
|
||||
- (NSUInteger)certificateRuleCount {
|
||||
- (int64_t)certificateRuleCount {
|
||||
return [self ruleCountForRuleType:SNTRuleTypeCertificate];
|
||||
}
|
||||
|
||||
- (NSUInteger)compilerRuleCount {
|
||||
- (int64_t)compilerRuleCount {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count =
|
||||
@@ -296,7 +296,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSUInteger)transitiveRuleCount {
|
||||
- (int64_t)transitiveRuleCount {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count =
|
||||
@@ -305,14 +305,18 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSUInteger)teamIDRuleCount {
|
||||
- (int64_t)teamIDRuleCount {
|
||||
return [self ruleCountForRuleType:SNTRuleTypeTeamID];
|
||||
}
|
||||
|
||||
- (NSUInteger)signingIDRuleCount {
|
||||
- (int64_t)signingIDRuleCount {
|
||||
return [self ruleCountForRuleType:SNTRuleTypeSigningID];
|
||||
}
|
||||
|
||||
- (int64_t)cdhashRuleCount {
|
||||
return [self ruleCountForRuleType:SNTRuleTypeCDHash];
|
||||
}
|
||||
|
||||
- (SNTRule *)ruleFromResultSet:(FMResultSet *)rs {
|
||||
SNTRule *r = [[SNTRule alloc] initWithIdentifier:[rs stringForColumn:@"identifier"]
|
||||
state:[rs intForColumn:@"state"]
|
||||
@@ -323,10 +327,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
return r;
|
||||
}
|
||||
|
||||
- (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256
|
||||
signingID:(NSString *)signingID
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID {
|
||||
- (SNTRule *)ruleForIdentifiers:(struct RuleIdentifiers)identifiers {
|
||||
__block SNTRule *rule;
|
||||
|
||||
// Look for a static rule that matches.
|
||||
@@ -334,22 +335,27 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
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];
|
||||
rule = staticRules[identifiers.cdhash];
|
||||
if (rule.type == SNTRuleTypeCDHash) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[identifiers.binarySHA256];
|
||||
if (rule.type == SNTRuleTypeBinary) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[signingID];
|
||||
rule = staticRules[identifiers.signingID];
|
||||
if (rule.type == SNTRuleTypeSigningID) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[certificateSHA256];
|
||||
rule = staticRules[identifiers.certificateSHA256];
|
||||
if (rule.type == SNTRuleTypeCertificate) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
rule = staticRules[teamID];
|
||||
rule = staticRules[identifiers.teamID];
|
||||
if (rule.type == SNTRuleTypeTeamID) {
|
||||
return rule;
|
||||
}
|
||||
@@ -360,9 +366,9 @@ 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 > Signing IDs > Certificates > Team IDs.
|
||||
// The intended order of precedence is CDHash > Binaries > Signing IDs > Certificates > Team IDs.
|
||||
//
|
||||
// As such the query should have "ORDER BY type DESC" before the LIMIT, to ensure that is the
|
||||
// As such the query should have "ORDER BY type ASC" before the LIMIT, to ensure that is the
|
||||
// case. However, in all tested versions of SQLite that ORDER BY clause is unnecessary: the query
|
||||
// is performed 'as written' by doing separate lookups in the index and the later lookups are if
|
||||
// the first returns a result. That behavior can be checked here: http://sqlfiddle.com/#!5/cdc42/1
|
||||
@@ -375,12 +381,15 @@ 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=1000) "
|
||||
@"OR (identifier=? AND type=2000) "
|
||||
@"OR (identifier=? AND type=3000) "
|
||||
@"OR (identifier=? AND type=4000) LIMIT 1",
|
||||
binarySHA256, signingID, certificateSHA256, teamID];
|
||||
FMResultSet *rs =
|
||||
[db executeQuery:@"SELECT * FROM rules WHERE "
|
||||
@" (identifier=? AND type=500) "
|
||||
@"OR (identifier=? AND type=1000) "
|
||||
@"OR (identifier=? AND type=2000) "
|
||||
@"OR (identifier=? AND type=3000) "
|
||||
@"OR (identifier=? AND type=4000) LIMIT 1",
|
||||
identifiers.cdhash, identifiers.binarySHA256, identifiers.signingID,
|
||||
identifiers.certificateSHA256, identifiers.teamID];
|
||||
if ([rs next]) {
|
||||
rule = [self ruleFromResultSet:rs];
|
||||
}
|
||||
@@ -389,8 +398,8 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
|
||||
// Allow binaries signed by the "Software Signing" cert used to sign launchd
|
||||
// if no existing rule has matched.
|
||||
if (!rule && [certificateSHA256 isEqual:self.launchdCSInfo.leafCertificate.SHA256]) {
|
||||
rule = [[SNTRule alloc] initWithIdentifier:certificateSHA256
|
||||
if (!rule && [identifiers.certificateSHA256 isEqual:self.launchdCSInfo.leafCertificate.SHA256]) {
|
||||
rule = [[SNTRule alloc] initWithIdentifier:identifiers.certificateSHA256
|
||||
state:SNTRuleStateAllow
|
||||
type:SNTRuleTypeCertificate
|
||||
customMsg:nil
|
||||
@@ -454,25 +463,59 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
}
|
||||
|
||||
- (BOOL)addedRulesShouldFlushDecisionCache:(NSArray *)rules {
|
||||
// Check for non-plain-allowlist rules first before querying the database.
|
||||
uint64_t nonAllowRuleCount = 0;
|
||||
|
||||
for (SNTRule *rule in rules) {
|
||||
if (rule.state != SNTRuleStateAllow) return YES;
|
||||
// If the rule is a remove rule, act conservatively and flush the cache.
|
||||
// This is to make sure cached rules of different precedence rules do not
|
||||
// impact final decision.
|
||||
if (rule.state == SNTRuleStateRemove) {
|
||||
return YES;
|
||||
}
|
||||
if (rule.state != SNTRuleStateAllow) {
|
||||
nonAllowRuleCount++;
|
||||
|
||||
// Just flush if we more than 1000 block rules.
|
||||
if (nonAllowRuleCount >= 1000) return YES;
|
||||
}
|
||||
}
|
||||
|
||||
// If still here, then all rules in the array are allowlist rules. So now we look for allowlist
|
||||
// rules where there is a previously existing allowlist compiler rule for the same identifier.
|
||||
// If so we find such a rule, then cache should be flushed.
|
||||
// Check newly synced rules for any blocking rules. If any are found, check
|
||||
// in the db to see if they already exist. If they're not found or were
|
||||
// previously allow rules flush the cache.
|
||||
//
|
||||
// If all rules in the array are allowlist rules, look for allowlist rules
|
||||
// where there is a previously existing allowlist compiler rule for the same
|
||||
// identifier. If so we find such a rule, then cache should be flushed.
|
||||
__block BOOL flushDecisionCache = NO;
|
||||
|
||||
[self inTransaction:^(FMDatabase *db, BOOL *rollback) {
|
||||
for (SNTRule *rule in rules) {
|
||||
// Allowlist certificate rules are ignored
|
||||
if (rule.type == SNTRuleTypeCertificate) continue;
|
||||
// If the rule is a block rule, silent block rule, or a compiler rule check if it already
|
||||
// exists in the database.
|
||||
//
|
||||
// If it does not then flush the cache. To ensure that the new rule is honored.
|
||||
if ((rule.state != SNTRuleStateAllow)) {
|
||||
if ([db longForQuery:
|
||||
@"SELECT COUNT(*) FROM rules WHERE identifier=? AND type=? AND state=? LIMIT 1",
|
||||
rule.identifier, @(rule.type), @(rule.state)] == 0) {
|
||||
flushDecisionCache = YES;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// At this point we know the rule is an allowlist rule. Check if it's
|
||||
// overriding a compiler rule.
|
||||
|
||||
if ([db longForQuery:
|
||||
@"SELECT COUNT(*) FROM rules WHERE identifier=? AND type=? AND state=? LIMIT 1",
|
||||
rule.identifier, @(SNTRuleTypeBinary), @(SNTRuleStateAllowCompiler)] > 0) {
|
||||
flushDecisionCache = YES;
|
||||
break;
|
||||
// Skip certificate and TeamID rules as they cannot be compiler rules.
|
||||
if (rule.type == SNTRuleTypeCertificate || rule.type == SNTRuleTypeTeamID) continue;
|
||||
|
||||
if ([db longForQuery:@"SELECT COUNT(*) FROM rules WHERE identifier=? AND type IN (?, ?, ?)"
|
||||
@" AND state=? LIMIT 1",
|
||||
rule.identifier, @(SNTRuleTypeCDHash), @(SNTRuleTypeBinary),
|
||||
@(SNTRuleTypeSigningID), @(SNTRuleStateAllowCompiler)] > 0) {
|
||||
flushDecisionCache = YES;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
@@ -540,7 +583,6 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
*error = [NSError errorWithDomain:@"com.google.santad.ruletable" code:code userInfo:d];
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark Querying
|
||||
|
||||
// Retrieve all rules from the Database
|
||||
|
||||
@@ -14,15 +14,19 @@
|
||||
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
|
||||
/// This test case actually tests SNTRuleTable and SNTRule
|
||||
@interface SNTRuleTableTest : XCTestCase
|
||||
@property SNTRuleTable *sut;
|
||||
@property FMDatabaseQueue *dbq;
|
||||
@property id mockConfigurator;
|
||||
@end
|
||||
|
||||
@implementation SNTRuleTableTest
|
||||
@@ -32,6 +36,13 @@
|
||||
|
||||
self.dbq = [[FMDatabaseQueue alloc] init];
|
||||
self.sut = [[SNTRuleTable alloc] initWithDatabaseQueue:self.dbq];
|
||||
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[self.mockConfigurator stopMocking];
|
||||
}
|
||||
|
||||
- (SNTRule *)_exampleTeamIDRule {
|
||||
@@ -56,6 +67,15 @@
|
||||
return r;
|
||||
}
|
||||
|
||||
- (SNTRule *)_exampleCDHashRule {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
r.identifier = @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a";
|
||||
r.state = SNTRuleStateBlock;
|
||||
r.type = SNTRuleTypeCDHash;
|
||||
r.customMsg = @"A cdhash rule";
|
||||
return r;
|
||||
}
|
||||
|
||||
- (SNTRule *)_exampleBinaryRule {
|
||||
SNTRule *r = [[SNTRule alloc] init];
|
||||
r.identifier = @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670";
|
||||
@@ -173,20 +193,20 @@
|
||||
error:nil];
|
||||
|
||||
SNTRule *r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.binarySHA256 =
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
}];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeBinary);
|
||||
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"b6ee1c3c5a715c049d14a8457faa6b6701b8507efe908300e238e0768bd759c2"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.binarySHA256 =
|
||||
@"b6ee1c3c5a715c049d14a8457faa6b6701b8507efe908300e238e0768bd759c2",
|
||||
}];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
@@ -196,20 +216,20 @@
|
||||
error:nil];
|
||||
|
||||
SNTRule *r = [self.sut
|
||||
ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:nil];
|
||||
ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.certificateSHA256 =
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258",
|
||||
}];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCertificate);
|
||||
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:@"5bdab1288fc16892fef50c658db54f1e2e19cf8f71cc55f77de2b95e051e2562"
|
||||
teamID:nil];
|
||||
ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.certificateSHA256 =
|
||||
@"5bdab1288fc16892fef50c658db54f1e2e19cf8f71cc55f77de2b95e051e2562",
|
||||
}];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
@@ -218,19 +238,17 @@
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:nil];
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
SNTRule *r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.teamID = @"ABCDEFGHIJ",
|
||||
}];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeTeamID);
|
||||
XCTAssertEqual([self.sut teamIDRuleCount], 1);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@"nonexistentTeamID"];
|
||||
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.teamID = @"nonexistentTeamID",
|
||||
}];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
@@ -244,79 +262,141 @@
|
||||
|
||||
XCTAssertEqual([self.sut signingIDRuleCount], 2);
|
||||
|
||||
SNTRule *r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:@"ABCDEFGHIJ:signingID"
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
SNTRule *r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.signingID = @"ABCDEFGHIJ:signingID",
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil
|
||||
signingID:@"platform:signingID"
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.signingID = @"platform:signingID",
|
||||
}];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"platform:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID);
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:nil signingID:@"nonexistent" certificateSHA256:nil teamID:nil];
|
||||
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.signingID = @"nonexistent",
|
||||
}];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
- (void)testFetchCDHashRule {
|
||||
[self.sut
|
||||
addRules:@[ [self _exampleBinaryRule], [self _exampleTeamIDRule], [self _exampleCDHashRule] ]
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:nil];
|
||||
|
||||
XCTAssertEqual([self.sut cdhashRuleCount], 1);
|
||||
|
||||
SNTRule *r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.cdhash = @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a",
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCDHash);
|
||||
|
||||
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.cdhash = @"nonexistent",
|
||||
}];
|
||||
XCTAssertNil(r);
|
||||
}
|
||||
|
||||
- (void)testFetchRuleOrdering {
|
||||
NSError *err;
|
||||
[self.sut addRules:@[
|
||||
[self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO]
|
||||
[self _exampleCertRule],
|
||||
[self _exampleBinaryRule],
|
||||
[self _exampleTeamIDRule],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO],
|
||||
[self _exampleCDHashRule],
|
||||
]
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:nil];
|
||||
error:&err];
|
||||
XCTAssertNil(err);
|
||||
|
||||
// This test verifies that the implicit rule ordering we've been abusing is still working.
|
||||
// See the comment in SNTRuleTable#ruleForBinarySHA256:certificateSHA256:teamID
|
||||
// See the comment in SNTRuleTable#ruleForIdentifiers:
|
||||
SNTRule *r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:@"ABCDEFGHIJ:signingID"
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.cdhash = @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a",
|
||||
.binarySHA256 =
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
.signingID = @"ABCDEFGHIJ:signingID",
|
||||
.certificateSHA256 =
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258",
|
||||
.teamID = @"ABCDEFGHIJ",
|
||||
}];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCDHash, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut
|
||||
ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.cdhash = @"unknown",
|
||||
.binarySHA256 =
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
.signingID = @"ABCDEFGHIJ:signingID",
|
||||
.certificateSHA256 =
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258",
|
||||
.teamID = @"ABCDEFGHIJ",
|
||||
}];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeBinary, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
|
||||
signingID:@"ABCDEFGHIJ:signingID"
|
||||
certificateSHA256:@"unknowncert"
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.cdhash = @"unknown",
|
||||
.binarySHA256 =
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
|
||||
.signingID = @"ABCDEFGHIJ:signingID",
|
||||
.certificateSHA256 = @"unknown",
|
||||
.teamID = @"ABCDEFGHIJ",
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeBinary, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut
|
||||
ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"unknown"
|
||||
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.cdhash = @"unknown",
|
||||
.binarySHA256 = @"unknown",
|
||||
.signingID = @"unknown",
|
||||
.certificateSHA256 =
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258",
|
||||
.teamID = @"ABCDEFGHIJ",
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier,
|
||||
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeCertificate, @"Implicit rule ordering failed");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"ABCDEFGHIJ:signingID"
|
||||
certificateSHA256:@"unknown"
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.cdhash = @"unknown",
|
||||
.binarySHA256 = @"unknown",
|
||||
.signingID = @"ABCDEFGHIJ:signingID",
|
||||
.certificateSHA256 = @"unknown",
|
||||
.teamID = @"ABCDEFGHIJ",
|
||||
}];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ:signingID");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeSigningID, @"Implicit rule ordering failed (SigningID)");
|
||||
|
||||
r = [self.sut ruleForBinarySHA256:@"unknown"
|
||||
signingID:@"unknown"
|
||||
certificateSHA256:@"unknown"
|
||||
teamID:@"ABCDEFGHIJ"];
|
||||
r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.cdhash = @"unknown",
|
||||
.binarySHA256 = @"unknown",
|
||||
.signingID = @"unknown",
|
||||
.certificateSHA256 = @"unknown",
|
||||
.teamID = @"ABCDEFGHIJ",
|
||||
}];
|
||||
XCTAssertNotNil(r);
|
||||
XCTAssertEqualObjects(r.identifier, @"ABCDEFGHIJ");
|
||||
XCTAssertEqual(r.type, SNTRuleTypeTeamID, @"Implicit rule ordering failed (TeamID)");
|
||||
@@ -342,18 +422,95 @@
|
||||
|
||||
- (void)testRetrieveAllRulesWithMultipleRules {
|
||||
[self.sut addRules:@[
|
||||
[self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO]
|
||||
[self _exampleCertRule],
|
||||
[self _exampleBinaryRule],
|
||||
[self _exampleTeamIDRule],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO],
|
||||
[self _exampleCDHashRule],
|
||||
]
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:nil];
|
||||
|
||||
NSArray<SNTRule *> *rules = [self.sut retrieveAllRules];
|
||||
XCTAssertEqual(rules.count, 4);
|
||||
XCTAssertEqual(rules.count, 5);
|
||||
XCTAssertEqualObjects(rules[0], [self _exampleCertRule]);
|
||||
XCTAssertEqualObjects(rules[1], [self _exampleBinaryRule]);
|
||||
XCTAssertEqualObjects(rules[2], [self _exampleTeamIDRule]);
|
||||
XCTAssertEqualObjects(rules[3], [self _exampleSigningIDRuleIsPlatform:NO]);
|
||||
XCTAssertEqualObjects(rules[4], [self _exampleCDHashRule]);
|
||||
}
|
||||
|
||||
- (void)testAddedRulesShouldFlushDecisionCacheWithNewBlockRule {
|
||||
// Ensure that a brand new block rule flushes the decision cache.
|
||||
NSError *error;
|
||||
SNTRule *r = [self _exampleBinaryRule];
|
||||
[self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error];
|
||||
XCTAssertNil(error);
|
||||
XCTAssertEqual(self.sut.ruleCount, 1);
|
||||
XCTAssertEqual(self.sut.binaryRuleCount, 1);
|
||||
|
||||
// Change the identifer so that the hash of a block rule is not found in the
|
||||
// db.
|
||||
r.identifier = @"bfff7d3f6c389ebf7a76a666c484d42ea447834901bc29141439ae7c7b96ff09";
|
||||
XCTAssertEqual(YES, [self.sut addedRulesShouldFlushDecisionCache:@[ r ]]);
|
||||
}
|
||||
|
||||
// Ensure that a brand new block rule flushes the decision cache.
|
||||
- (void)testAddedRulesShouldFlushDecisionCacheWithOldBlockRule {
|
||||
NSError *error;
|
||||
SNTRule *r = [self _exampleBinaryRule];
|
||||
[self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error];
|
||||
XCTAssertNil(error);
|
||||
XCTAssertEqual(self.sut.ruleCount, 1);
|
||||
XCTAssertEqual(self.sut.binaryRuleCount, 1);
|
||||
XCTAssertEqual(NO, [self.sut addedRulesShouldFlushDecisionCache:@[ r ]]);
|
||||
}
|
||||
|
||||
// Ensure that a larger number of blocks flushes the decision cache.
|
||||
- (void)testAddedRulesShouldFlushDecisionCacheWithLargeNumberOfBlocks {
|
||||
NSError *error;
|
||||
SNTRule *r = [self _exampleBinaryRule];
|
||||
[self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error];
|
||||
XCTAssertNil(error);
|
||||
XCTAssertEqual(self.sut.ruleCount, 1);
|
||||
XCTAssertEqual(self.sut.binaryRuleCount, 1);
|
||||
NSMutableArray<SNTRule *> *newRules = [NSMutableArray array];
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
newRules[i] = r;
|
||||
}
|
||||
|
||||
XCTAssertEqual(YES, [self.sut addedRulesShouldFlushDecisionCache:newRules]);
|
||||
}
|
||||
|
||||
// Ensure that an allow rule that overrides a compiler rule flushes the
|
||||
// decision cache.
|
||||
- (void)testAddedRulesShouldFlushDecisionCacheWithCompilerRule {
|
||||
NSError *error;
|
||||
SNTRule *r = [self _exampleBinaryRule];
|
||||
r.type = SNTRuleTypeBinary;
|
||||
r.state = SNTRuleStateAllowCompiler;
|
||||
[self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error];
|
||||
XCTAssertNil(error);
|
||||
XCTAssertEqual(self.sut.ruleCount, 1);
|
||||
XCTAssertEqual(self.sut.binaryRuleCount, 1);
|
||||
// make the rule an allow rule
|
||||
r.state = SNTRuleStateAllow;
|
||||
XCTAssertEqual(YES, [self.sut addedRulesShouldFlushDecisionCache:@[ r ]]);
|
||||
}
|
||||
|
||||
// Ensure that an Remove rule targeting an allow rule causes a flush of the cache.
|
||||
- (void)testAddedRulesShouldFlushDecisionCacheWithRemoveRule {
|
||||
NSError *error;
|
||||
SNTRule *r = [self _exampleBinaryRule];
|
||||
r.type = SNTRuleTypeBinary;
|
||||
r.state = SNTRuleStateAllow;
|
||||
[self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error];
|
||||
XCTAssertNil(error);
|
||||
XCTAssertEqual(self.sut.ruleCount, 1);
|
||||
XCTAssertEqual(self.sut.binaryRuleCount, 1);
|
||||
|
||||
r.state = SNTRuleStateRemove;
|
||||
XCTAssertEqual(YES, [self.sut addedRulesShouldFlushDecisionCache:@[ r ]]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -152,13 +152,13 @@ void DARegisterDiskAppearedCallback(DASessionRef session, CFDictionaryRef __null
|
||||
|
||||
void DARegisterDiskDisappearedCallback(DASessionRef session, CFDictionaryRef __nullable match,
|
||||
DADiskDisappearedCallback callback,
|
||||
void *__nullable context){};
|
||||
void *__nullable context) {};
|
||||
|
||||
void DARegisterDiskDescriptionChangedCallback(DASessionRef session,
|
||||
CFDictionaryRef __nullable match,
|
||||
CFArrayRef __nullable watch,
|
||||
DADiskDescriptionChangedCallback callback,
|
||||
void *__nullable context){};
|
||||
void *__nullable context) {};
|
||||
|
||||
void DASessionSetDispatchQueue(DASessionRef session, dispatch_queue_t __nullable queue) {
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <variant>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.pb.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
@@ -71,25 +72,30 @@ class EnrichedProcess {
|
||||
: effective_user_(std::nullopt),
|
||||
effective_group_(std::nullopt),
|
||||
real_user_(std::nullopt),
|
||||
real_group_(std::nullopt) {}
|
||||
real_group_(std::nullopt),
|
||||
annotations_(std::nullopt) {}
|
||||
|
||||
EnrichedProcess(std::optional<std::shared_ptr<std::string>> &&effective_user,
|
||||
std::optional<std::shared_ptr<std::string>> &&effective_group,
|
||||
std::optional<std::shared_ptr<std::string>> &&real_user,
|
||||
std::optional<std::shared_ptr<std::string>> &&real_group,
|
||||
EnrichedFile &&executable)
|
||||
EnrichedProcess(
|
||||
std::optional<std::shared_ptr<std::string>> &&effective_user,
|
||||
std::optional<std::shared_ptr<std::string>> &&effective_group,
|
||||
std::optional<std::shared_ptr<std::string>> &&real_user,
|
||||
std::optional<std::shared_ptr<std::string>> &&real_group,
|
||||
EnrichedFile &&executable,
|
||||
std::optional<santa::pb::v1::process_tree::Annotations> &&annotations)
|
||||
: effective_user_(std::move(effective_user)),
|
||||
effective_group_(std::move(effective_group)),
|
||||
real_user_(std::move(real_user)),
|
||||
real_group_(std::move(real_group)),
|
||||
executable_(std::move(executable)) {}
|
||||
executable_(std::move(executable)),
|
||||
annotations_(std::move(annotations)) {}
|
||||
|
||||
EnrichedProcess(EnrichedProcess &&other)
|
||||
: effective_user_(std::move(other.effective_user_)),
|
||||
effective_group_(std::move(other.effective_group_)),
|
||||
real_user_(std::move(other.real_user_)),
|
||||
real_group_(std::move(other.real_group_)),
|
||||
executable_(std::move(other.executable_)) {}
|
||||
executable_(std::move(other.executable_)),
|
||||
annotations_(std::move(other.annotations_)) {}
|
||||
|
||||
// Note: Move assignment could be safely implemented but not currently needed
|
||||
EnrichedProcess &operator=(EnrichedProcess &&other) = delete;
|
||||
@@ -110,6 +116,10 @@ class EnrichedProcess {
|
||||
return real_group_;
|
||||
}
|
||||
const EnrichedFile &executable() const { return executable_; }
|
||||
const std::optional<santa::pb::v1::process_tree::Annotations> &annotations()
|
||||
const {
|
||||
return annotations_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<std::shared_ptr<std::string>> effective_user_;
|
||||
@@ -117,6 +127,7 @@ class EnrichedProcess {
|
||||
std::optional<std::shared_ptr<std::string>> real_user_;
|
||||
std::optional<std::shared_ptr<std::string>> real_group_;
|
||||
EnrichedFile executable_;
|
||||
std::optional<santa::pb::v1::process_tree::Annotations> annotations_;
|
||||
};
|
||||
|
||||
class EnrichedEventType {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
#include "Source/common/SantaCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
@@ -32,7 +33,7 @@ enum class EnrichOptions {
|
||||
|
||||
class Enricher {
|
||||
public:
|
||||
Enricher();
|
||||
Enricher(std::shared_ptr<process_tree::ProcessTree> pt = nullptr);
|
||||
virtual ~Enricher() = default;
|
||||
virtual std::unique_ptr<EnrichedMessage> Enrich(Message &&msg);
|
||||
virtual EnrichedProcess Enrich(
|
||||
@@ -51,6 +52,7 @@ class Enricher {
|
||||
username_cache_;
|
||||
SantaCache<gid_t, std::optional<std::shared_ptr<std::string>>>
|
||||
groupname_cache_;
|
||||
std::shared_ptr<process_tree::ProcessTree> process_tree_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
@@ -25,10 +25,14 @@
|
||||
|
||||
#include "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/ProcessTree/SNTEndpointSecurityAdapter.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
#include "Source/santad/ProcessTree/process_tree_macos.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Enricher::Enricher() : username_cache_(256), groupname_cache_(256) {}
|
||||
Enricher::Enricher(std::shared_ptr<::santa::santad::process_tree::ProcessTree> pt)
|
||||
: username_cache_(256), groupname_cache_(256), process_tree_(std::move(pt)) {}
|
||||
|
||||
std::unique_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
|
||||
// TODO(mlw): Consider potential design patterns that could help reduce memory usage under load
|
||||
@@ -89,7 +93,10 @@ EnrichedProcess Enricher::Enrich(const es_process_t &es_proc, EnrichOptions opti
|
||||
UsernameForGID(audit_token_to_egid(es_proc.audit_token), options),
|
||||
UsernameForUID(audit_token_to_ruid(es_proc.audit_token), options),
|
||||
UsernameForGID(audit_token_to_rgid(es_proc.audit_token), options),
|
||||
Enrich(*es_proc.executable, options));
|
||||
Enrich(*es_proc.executable, options),
|
||||
process_tree_ ? process_tree_->ExportAnnotations(
|
||||
process_tree::PidFromAuditToken(es_proc.audit_token))
|
||||
: std::nullopt);
|
||||
}
|
||||
|
||||
EnrichedFile Enricher::Enrich(const es_file_t &es_file, EnrichOptions options) {
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class EndpointSecurityAPI;
|
||||
@@ -37,15 +39,24 @@ class Message {
|
||||
Message(const Message& other);
|
||||
Message& operator=(const Message& other) = delete;
|
||||
|
||||
void SetProcessToken(process_tree::ProcessToken tok);
|
||||
|
||||
// Operators to access underlying es_message_t
|
||||
const es_message_t* operator->() const { return es_msg_; }
|
||||
const es_message_t& operator*() const { return *es_msg_; }
|
||||
|
||||
// Helper to get the API associated with this message.
|
||||
// Used for things like es_exec_arg_count.
|
||||
// We should ideally rework this to somehow present these functions as methods
|
||||
// on the Message, however this would be a bit of a bigger lift.
|
||||
std::shared_ptr<EndpointSecurityAPI> ESAPI() const { return esapi_; }
|
||||
|
||||
std::string ParentProcessName() const;
|
||||
|
||||
private:
|
||||
std::shared_ptr<EndpointSecurityAPI> esapi_;
|
||||
const es_message_t* es_msg_;
|
||||
std::optional<process_tree::ProcessToken> process_token_;
|
||||
|
||||
std::string GetProcessName(pid_t pid) const;
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Message::Message(std::shared_ptr<EndpointSecurityAPI> esapi, const es_message_t *es_msg)
|
||||
: esapi_(std::move(esapi)), es_msg_(es_msg) {
|
||||
: esapi_(std::move(esapi)), es_msg_(es_msg), process_token_(std::nullopt) {
|
||||
esapi_->RetainMessage(es_msg);
|
||||
}
|
||||
|
||||
@@ -36,12 +36,19 @@ Message::Message(Message &&other) {
|
||||
esapi_ = std::move(other.esapi_);
|
||||
es_msg_ = other.es_msg_;
|
||||
other.es_msg_ = nullptr;
|
||||
process_token_ = std::move(other.process_token_);
|
||||
other.process_token_ = std::nullopt;
|
||||
}
|
||||
|
||||
Message::Message(const Message &other) {
|
||||
esapi_ = other.esapi_;
|
||||
es_msg_ = other.es_msg_;
|
||||
esapi_->RetainMessage(es_msg_);
|
||||
process_token_ = other.process_token_;
|
||||
}
|
||||
|
||||
void Message::SetProcessToken(process_tree::ProcessToken tok) {
|
||||
process_token_ = std::move(tok);
|
||||
}
|
||||
|
||||
std::string Message::ParentProcessName() const {
|
||||
|
||||
@@ -21,11 +21,13 @@
|
||||
#include <stdlib.h>
|
||||
#include <sys/qos.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "Source/common/BranchPrediction.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/common/SystemResources.h"
|
||||
@@ -48,7 +50,9 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
"/private/var/db/santa/events.db"};
|
||||
|
||||
@interface SNTEndpointSecurityClient ()
|
||||
@property int64_t deadlineMarginMS;
|
||||
@property(nonatomic) double defaultBudget;
|
||||
@property(nonatomic) int64_t minAllowedHeadroom;
|
||||
@property(nonatomic) int64_t maxAllowedHeadroom;
|
||||
@property SNTConfigurator *configurator;
|
||||
@end
|
||||
|
||||
@@ -68,10 +72,18 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
if (self) {
|
||||
_esApi = std::move(esApi);
|
||||
_metrics = std::move(metrics);
|
||||
_deadlineMarginMS = 5000;
|
||||
_configurator = [SNTConfigurator configurator];
|
||||
_processor = processor;
|
||||
|
||||
// Default event processing budget is 80% of the deadline time
|
||||
_defaultBudget = 0.8;
|
||||
|
||||
// For events with small deadlines, clamp processing budget to 1s headroom
|
||||
_minAllowedHeadroom = 1 * NSEC_PER_SEC;
|
||||
|
||||
// For events with large deadlines, clamp processing budget to 5s headroom
|
||||
_maxAllowedHeadroom = 5 * NSEC_PER_SEC;
|
||||
|
||||
_authQueue = dispatch_queue_create(
|
||||
"com.google.santa.daemon.auth_queue",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL,
|
||||
@@ -116,6 +128,10 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (bool)handleContextMessage:(Message &)esMsg {
|
||||
return false;
|
||||
}
|
||||
|
||||
- (void)establishClientOrDie {
|
||||
if (self->_esClient.IsConnected()) {
|
||||
// This is a programming error
|
||||
@@ -131,6 +147,14 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
self->_metrics->UpdateEventStats(self->_processor, esMsg.operator->());
|
||||
|
||||
es_event_type_t eventType = esMsg->event_type;
|
||||
|
||||
if ([self handleContextMessage:esMsg]) {
|
||||
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
self->_metrics->SetEventMetrics(self->_processor, eventType, EventDisposition::kProcessed,
|
||||
processingEnd - processingStart);
|
||||
return;
|
||||
}
|
||||
|
||||
if ([self shouldHandleMessage:esMsg]) {
|
||||
[self handleMessage:std::move(esMsg)
|
||||
recordEventMetrics:^(EventDisposition disposition) {
|
||||
@@ -255,6 +279,24 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
});
|
||||
}
|
||||
|
||||
- (int64_t)computeBudgetForDeadline:(uint64_t)deadline currentTime:(uint64_t)currentTime {
|
||||
// First get how much time we have left
|
||||
int64_t nanosUntilDeadline = (int64_t)MachTimeToNanos(deadline - currentTime);
|
||||
|
||||
// Compute the desired budget
|
||||
int64_t budget = nanosUntilDeadline * self.defaultBudget;
|
||||
|
||||
// See how much headroom is left
|
||||
int64_t headroom = nanosUntilDeadline - budget;
|
||||
|
||||
// Clamp headroom to maximize budget but ensure it's not so large as to not leave
|
||||
// enough time to respond in an emergency.
|
||||
headroom = std::clamp(headroom, self.minAllowedHeadroom, self.maxAllowedHeadroom);
|
||||
|
||||
// Return the processing budget given the allotted headroom
|
||||
return nanosUntilDeadline - headroom;
|
||||
}
|
||||
|
||||
- (void)processMessage:(Message &&)msg handler:(void (^)(const Message &))messageHandler {
|
||||
if (unlikely(msg->action_type != ES_ACTION_TYPE_AUTH)) {
|
||||
// This is a programming error
|
||||
@@ -270,33 +312,33 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
dispatch_semaphore_signal(processingSema);
|
||||
dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0);
|
||||
|
||||
const uint64_t timeout = NSEC_PER_MSEC * (self.deadlineMarginMS);
|
||||
|
||||
uint64_t deadlineNano = MachTimeToNanos(msg->deadline - mach_absolute_time());
|
||||
|
||||
// TODO(mlw): How should we handle `deadlineNano <= timeout`. Will currently
|
||||
// result in the deadline block being dispatched immediately (and therefore
|
||||
// the event will be denied).
|
||||
int64_t processingBudget = [self computeBudgetForDeadline:msg->deadline
|
||||
currentTime:mach_absolute_time()];
|
||||
|
||||
// Workaround for compiler bug that doesn't properly close over variables
|
||||
__block Message processMsg = msg;
|
||||
__block Message deadlineMsg = msg;
|
||||
|
||||
dispatch_after(
|
||||
dispatch_time(DISPATCH_TIME_NOW, deadlineNano - timeout), self->_authQueue, ^(void) {
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Handler has already responded, nothing to do.
|
||||
return;
|
||||
}
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, processingBudget), self->_authQueue, ^(void) {
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Handler has already responded, nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
bool res = [self respondToMessage:deadlineMsg
|
||||
withAuthResult:ES_AUTH_RESULT_DENY
|
||||
cacheable:false];
|
||||
es_auth_result_t authResult;
|
||||
if (self.configurator.failClosed) {
|
||||
authResult = ES_AUTH_RESULT_DENY;
|
||||
} else {
|
||||
authResult = ES_AUTH_RESULT_ALLOW;
|
||||
}
|
||||
|
||||
LOGE(@"SNTEndpointSecurityClient: deadline reached: deny pid=%d, event type: %d ret=%d",
|
||||
audit_token_to_pid(deadlineMsg->process->audit_token), deadlineMsg->event_type, res);
|
||||
dispatch_semaphore_signal(deadlineExpiredSema);
|
||||
});
|
||||
bool res = [self respondToMessage:deadlineMsg withAuthResult:authResult cacheable:false];
|
||||
|
||||
LOGE(@"SNTEndpointSecurityClient: deadline reached: pid=%d, event type: %d, result: %@, ret=%d",
|
||||
audit_token_to_pid(deadlineMsg->process->audit_token), deadlineMsg->event_type,
|
||||
(authResult == ES_AUTH_RESULT_DENY ? @"denied" : @"allowed"), res);
|
||||
dispatch_semaphore_signal(deadlineExpiredSema);
|
||||
});
|
||||
|
||||
dispatch_async(self->_authQueue, ^{
|
||||
messageHandler(processMsg);
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SystemResources.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
@@ -48,8 +50,11 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
- (void)handleMessage:(Message &&)esMsg
|
||||
recordEventMetrics:(void (^)(santa::santad::EventDisposition disposition))recordEventMetrics;
|
||||
- (BOOL)shouldHandleMessage:(const Message &)esMsg;
|
||||
- (int64_t)computeBudgetForDeadline:(uint64_t)deadline currentTime:(uint64_t)currentTime;
|
||||
|
||||
@property int64_t deadlineMarginMS;
|
||||
@property(nonatomic) double defaultBudget;
|
||||
@property(nonatomic) int64_t minAllowedHeadroom;
|
||||
@property(nonatomic) int64_t maxAllowedHeadroom;
|
||||
@end
|
||||
|
||||
@interface SNTEndpointSecurityClientTest : XCTestCase
|
||||
@@ -412,11 +417,11 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
{
|
||||
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)));
|
||||
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), std::nullopt),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
|
||||
[client processEnrichedMessage:std::move(enrichedMsg)
|
||||
handler:^(std::unique_ptr<EnrichedMessage> msg) {
|
||||
@@ -503,7 +508,47 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testProcessMessageHandlerWithDeadlineTimeout {
|
||||
- (void)testComputeBudgetForDeadlineCurrentTime {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
// The test uses crafted values to make even numbers. Ensure the client has
|
||||
// expected values for these properties so the test can fail early if not.
|
||||
XCTAssertEqual(client.defaultBudget, 0.8);
|
||||
XCTAssertEqual(client.minAllowedHeadroom, 1 * NSEC_PER_SEC);
|
||||
XCTAssertEqual(client.maxAllowedHeadroom, 5 * NSEC_PER_SEC);
|
||||
|
||||
std::map<uint64_t, int64_t> deadlineMillisToBudgetMillis{
|
||||
// Further out deadlines clamp processing budget to maxAllowedHeadroom
|
||||
{45000, 40000},
|
||||
|
||||
// Closer deadlines allow a set percentage processing budget
|
||||
{15000, 12000},
|
||||
|
||||
// Near deadlines clamp processing budget to minAllowedHeadroom
|
||||
{3500, 2500}};
|
||||
|
||||
uint64_t curTime = mach_absolute_time();
|
||||
|
||||
for (const auto [deadlineMS, budgetMS] : deadlineMillisToBudgetMillis) {
|
||||
int64_t got =
|
||||
[client computeBudgetForDeadline:AddNanosecondsToMachTime(deadlineMS * NSEC_PER_MSEC, curTime)
|
||||
currentTime:curTime];
|
||||
|
||||
// Add 100us, then clip to ms to account for non-exact values due to timebase division
|
||||
got = (int64_t)((double)(got + (100 * NSEC_PER_USEC)) / (double)NSEC_PER_MSEC);
|
||||
|
||||
XCTAssertEqual(got, budgetMS);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)checkDeadlineExpiredFailClosed:(BOOL)shouldFailClosed {
|
||||
// Set a es_message_t deadline of 750ms
|
||||
// Set a deadline leeway in the `SNTEndpointSecurityClient` of 500ms
|
||||
// Mock `RespondFlagsResult` which is called from the deadline handler
|
||||
@@ -517,7 +562,7 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
// deadlineSema is signaled (or a timeout waiting on deadlineSema)
|
||||
es_file_t proc_file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&proc_file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, &proc, ActionType::Auth,
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth,
|
||||
750); // 750ms timeout
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
@@ -526,18 +571,27 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
dispatch_semaphore_t deadlineSema = dispatch_semaphore_create(0);
|
||||
dispatch_semaphore_t controlSema = dispatch_semaphore_create(0);
|
||||
|
||||
EXPECT_CALL(*mockESApi, RespondFlagsResult(testing::_, testing::_, 0x0, false))
|
||||
es_auth_result_t wantAuthResult = shouldFailClosed ? ES_AUTH_RESULT_DENY : ES_AUTH_RESULT_ALLOW;
|
||||
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, wantAuthResult, false))
|
||||
.WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
// Signal deadlineSema to let the handler block continue execution
|
||||
dispatch_semaphore_signal(deadlineSema);
|
||||
return true;
|
||||
}));
|
||||
|
||||
id mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([mockConfigurator configurator]).andReturn(mockConfigurator);
|
||||
|
||||
OCMExpect([mockConfigurator failClosed]).andReturn(shouldFailClosed);
|
||||
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
client.deadlineMarginMS = 500;
|
||||
|
||||
// Set min/max headroom the same to clamp the value for this test
|
||||
client.minAllowedHeadroom = 500 * NSEC_PER_MSEC;
|
||||
client.maxAllowedHeadroom = 500 * NSEC_PER_MSEC;
|
||||
|
||||
{
|
||||
__block long result;
|
||||
@@ -566,7 +620,18 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
// seeing the warning (but still possible)
|
||||
SleepMS(100);
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockConfigurator));
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
|
||||
[mockConfigurator stopMocking];
|
||||
}
|
||||
|
||||
- (void)testDeadlineExpiredFailClosed {
|
||||
[self checkDeadlineExpiredFailClosed:YES];
|
||||
}
|
||||
|
||||
- (void)testDeadlineExpiredFailOpen {
|
||||
[self checkDeadlineExpiredFailClosed:NO];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
@@ -50,6 +51,12 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
MOCK_METHOD(void, FlushCache, (FlushCacheMode mode, FlushCacheReason reason));
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityClient (Testing)
|
||||
@property(nonatomic) double defaultBudget;
|
||||
@property(nonatomic) int64_t minAllowedHeadroom;
|
||||
@property(nonatomic) int64_t maxAllowedHeadroom;
|
||||
@end
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManager (Testing)
|
||||
- (instancetype)init;
|
||||
- (void)logDiskAppeared:(NSDictionary *)props;
|
||||
@@ -136,6 +143,11 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
|
||||
// This test is sensitive to ~1s processing budget.
|
||||
// Set a 5s headroom and 6s deadline
|
||||
deviceManager.minAllowedHeadroom = 5 * NSEC_PER_SEC;
|
||||
deviceManager.maxAllowedHeadroom = 5 * NSEC_PER_SEC;
|
||||
es_message_t esMsg = MakeESMessage(eventType, &proc, ActionType::Auth, 6000);
|
||||
|
||||
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
|
||||
|
||||
@@ -17,15 +17,17 @@
|
||||
#import "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityTreeAwareClient.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
#import "Source/santad/Metrics.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
#import "Source/santad/SNTCompilerController.h"
|
||||
|
||||
/// ES Client focused on subscribing to NOTIFY event variants with the intention of enriching
|
||||
/// received messages and logging the information.
|
||||
@interface SNTEndpointSecurityRecorder : SNTEndpointSecurityClient <SNTEndpointSecurityEventHandler>
|
||||
@interface SNTEndpointSecurityRecorder
|
||||
: SNTEndpointSecurityTreeAwareClient <SNTEndpointSecurityEventHandler>
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
@@ -38,6 +40,7 @@
|
||||
compilerController:(SNTCompilerController *)compilerController
|
||||
authResultCache:
|
||||
(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache
|
||||
prefixTree:(std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>>)prefixTree;
|
||||
prefixTree:(std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>>)prefixTree
|
||||
processTree:(std::shared_ptr<santa::santad::process_tree::ProcessTree>)processTree;
|
||||
|
||||
@end
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityRecorder.h"
|
||||
#include <os/base.h>
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
@@ -23,6 +24,7 @@
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
using santa::common::PrefixTree;
|
||||
using santa::common::Unit;
|
||||
@@ -33,10 +35,12 @@ using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
using santa::santad::process_tree::ProcessTree;
|
||||
|
||||
es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
switch (msg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: return msg->event.close.target;
|
||||
case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA: return msg->event.exchangedata.file1;
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK: return msg->event.link.source;
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: return msg->event.rename.source;
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK: return msg->event.unlink.target;
|
||||
@@ -62,10 +66,12 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
enricher:(std::shared_ptr<Enricher>)enricher
|
||||
compilerController:(SNTCompilerController *)compilerController
|
||||
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache
|
||||
prefixTree:(std::shared_ptr<PrefixTree<Unit>>)prefixTree {
|
||||
prefixTree:(std::shared_ptr<PrefixTree<Unit>>)prefixTree
|
||||
processTree:(std::shared_ptr<ProcessTree>)processTree {
|
||||
self = [super initWithESAPI:std::move(esApi)
|
||||
metrics:std::move(metrics)
|
||||
processor:santa::santad::Processor::kRecorder];
|
||||
processor:santa::santad::Processor::kRecorder
|
||||
processTree:std::move(processTree)];
|
||||
if (self) {
|
||||
_enricher = enricher;
|
||||
_logger = logger;
|
||||
@@ -91,10 +97,10 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
BOOL shouldLogClose = esMsg->event.close.modified;
|
||||
|
||||
#if HAVE_MACOS_13
|
||||
if (@available(macOS 13.5, *)) {
|
||||
if (esMsg->version >= 6) {
|
||||
// As of macSO 13.0 we have a new field for if a file was mmaped with
|
||||
// write permissions on close events. However it did not work until
|
||||
// 13.5.
|
||||
// write permissions on close events. However due to a bug in ES, it
|
||||
// only worked for certain conditions until macOS 13.5 (FB12094635).
|
||||
//
|
||||
// If something was mmaped writable it was probably written to. Often
|
||||
// developer tools do this to avoid lots of write syscalls, e.g. go's
|
||||
@@ -114,8 +120,28 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
|
||||
self->_authResultCache->RemoveFromCache(esMsg->event.close.target);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
[self.compilerController handleEvent:esMsg withLogger:self->_logger];
|
||||
|
||||
switch (esMsg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: OS_FALLTHROUGH;
|
||||
case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA: OS_FALLTHROUGH;
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK: OS_FALLTHROUGH;
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: OS_FALLTHROUGH;
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK: {
|
||||
es_file_t *targetFile = GetTargetFileForPrefixTree(&(*esMsg));
|
||||
|
||||
if (!targetFile) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Only log file changes that match the given regex
|
||||
NSString *targetPath = santa::common::StringToNSString(esMsg->event.close.target->path.data);
|
||||
NSString *targetPath = santa::common::StringToNSString(targetFile->path.data);
|
||||
if (![[self.configurator fileChangesRegex]
|
||||
numberOfMatchesInString:targetPath
|
||||
options:0
|
||||
@@ -127,27 +153,26 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->_prefixTree->HasPrefix(targetFile->path.data)) {
|
||||
recordEventMetrics(EventDisposition::kDropped);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK: OS_FALLTHROUGH;
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT: {
|
||||
if (self.configurator.enableForkAndExitLogging == NO) {
|
||||
recordEventMetrics(EventDisposition::kDropped);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
[self.compilerController handleEvent:esMsg withLogger:self->_logger];
|
||||
|
||||
if ((esMsg->event_type == ES_EVENT_TYPE_NOTIFY_FORK ||
|
||||
esMsg->event_type == ES_EVENT_TYPE_NOTIFY_EXIT) &&
|
||||
self.configurator.enableForkAndExitLogging == NO) {
|
||||
recordEventMetrics(EventDisposition::kDropped);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter file op events matching the prefix tree.
|
||||
es_file_t *targetFile = GetTargetFileForPrefixTree(&(*esMsg));
|
||||
if (targetFile != NULL && self->_prefixTree->HasPrefix(targetFile->path.data)) {
|
||||
recordEventMetrics(EventDisposition::kDropped);
|
||||
return;
|
||||
}
|
||||
|
||||
// Enrich the message inline with the ES handler block to capture enrichment
|
||||
// data as close to the source event as possible.
|
||||
std::unique_ptr<EnrichedMessage> enrichedMessage = _enricher->Enrich(std::move(esMsg));
|
||||
|
||||
@@ -103,7 +103,7 @@ class MockLogger : public Logger {
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
typedef void (^testHelperBlock)(es_message_t *message,
|
||||
typedef void (^TestHelperBlock)(es_message_t *message,
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient,
|
||||
std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
@@ -114,7 +114,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
|
||||
- (void)handleMessageShouldLog:(BOOL)shouldLog
|
||||
shouldRemoveFromCache:(BOOL)shouldRemoveFromCache
|
||||
withBlock:(testHelperBlock)testBlock {
|
||||
withBlock:(TestHelperBlock)testBlock {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc, ActionType::Auth);
|
||||
@@ -157,7 +157,8 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
enricher:mockEnricher
|
||||
compilerController:mockCC
|
||||
authResultCache:mockAuthCache
|
||||
prefixTree:prefixTree];
|
||||
prefixTree:prefixTree
|
||||
processTree:nullptr];
|
||||
|
||||
testBlock(&esMsg, mockESApi, mockCC, recorderClient, prefixTree, &sema, &semaMetrics);
|
||||
|
||||
@@ -176,7 +177,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
if (@available(macOS 13.0, *)) {
|
||||
// CLOSE not modified, but was_mapped_writable, should remove from cache,
|
||||
// and matches fileChangesRegex
|
||||
testHelperBlock testBlock =
|
||||
TestHelperBlock testBlock =
|
||||
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema,
|
||||
@@ -208,7 +209,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
if (@available(macOS 13.0, *)) {
|
||||
// CLOSE not modified, but was_mapped_writable, remove from cache, and does not match
|
||||
// fileChangesRegex
|
||||
testHelperBlock testBlock =
|
||||
TestHelperBlock testBlock =
|
||||
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema,
|
||||
@@ -232,7 +233,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
|
||||
- (void)testHandleMessage {
|
||||
// CLOSE not modified, bail early
|
||||
testHelperBlock testBlock = ^(
|
||||
TestHelperBlock testBlock = ^(
|
||||
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
|
||||
@@ -281,6 +282,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
esMsg->event.close.modified = true;
|
||||
esMsg->event.close.target = &targetFileMissesRegex;
|
||||
Message msg(mockESApi, esMsg);
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTFail("Metrics record callback should not be called here");
|
||||
@@ -289,6 +291,44 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
|
||||
[self handleMessageShouldLog:NO shouldRemoveFromCache:YES withBlock:testBlock];
|
||||
|
||||
// UNLINK, remove from cache, but doesn't match fileChangesRegex
|
||||
testBlock = ^(
|
||||
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_UNLINK;
|
||||
esMsg->event.unlink.target = &targetFileMissesRegex;
|
||||
Message msg(mockESApi, esMsg);
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTFail("Metrics record callback should not be called here");
|
||||
}]);
|
||||
};
|
||||
|
||||
[self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock];
|
||||
|
||||
// EXCHANGEDATA, Prefix match, bail early
|
||||
testBlock = ^(
|
||||
es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) {
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_UNLINK;
|
||||
esMsg->event.exchangedata.file1 = &targetFileMatchesRegex;
|
||||
prefixTree->InsertPrefix(esMsg->event.exchangedata.file1->path.data, Unit{});
|
||||
Message msg(mockESApi, esMsg);
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(*semaMetrics);
|
||||
}]);
|
||||
|
||||
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
};
|
||||
|
||||
[self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock];
|
||||
|
||||
// LINK, Prefix match, bail early
|
||||
testBlock =
|
||||
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
@@ -371,6 +411,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
extern es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg);
|
||||
|
||||
es_file_t closeFile = MakeESFile("close");
|
||||
es_file_t exchangedataFile = MakeESFile("exchangedata");
|
||||
es_file_t linkFile = MakeESFile("link");
|
||||
es_file_t renameFile = MakeESFile("rename");
|
||||
es_file_t unlinkFile = MakeESFile("unlink");
|
||||
@@ -393,7 +434,8 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &unlinkFile);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
|
||||
esMsg.event.exchangedata.file1 = &exchangedataFile;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &exchangedataFile);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/// Copyright 2024 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.
|
||||
|
||||
#include "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
@interface SNTEndpointSecurityTreeAwareClient : SNTEndpointSecurityClient
|
||||
@property std::shared_ptr<santa::santad::process_tree::ProcessTree> processTree;
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
processor:(santa::santad::Processor)processor
|
||||
processTree:(std::shared_ptr<santa::santad::process_tree::ProcessTree>)processTree;
|
||||
@end
|
||||
@@ -0,0 +1,114 @@
|
||||
/// Copyright 2024 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityTreeAwareClient.h"
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
#include "Source/santad/ProcessTree/SNTEndpointSecurityAdapter.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
#include "Source/santad/ProcessTree/process_tree_macos.h"
|
||||
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::Metrics;
|
||||
using santa::santad::Processor;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
@implementation SNTEndpointSecurityTreeAwareClient {
|
||||
std::vector<bool> _addedEvents;
|
||||
}
|
||||
|
||||
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<Metrics>)metrics
|
||||
processor:(Processor)processor
|
||||
processTree:
|
||||
(std::shared_ptr<santa::santad::process_tree::ProcessTree>)processTree {
|
||||
self = [super initWithESAPI:std::move(esApi) metrics:std::move(metrics) processor:processor];
|
||||
if (self) {
|
||||
_processTree = std::move(processTree);
|
||||
_addedEvents.resize(ES_EVENT_TYPE_LAST, false);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// ES guarantees logical consistency within a client (e.g. forks always precede exits),
|
||||
// however there are no guarantees about the ordering of when messages are delivered _across_
|
||||
// clients, meaning any client might be the first one to receive process events, and therefore would
|
||||
// need to be the one to inform the tree. However not all clients are interested in or subscribe to
|
||||
// process events. This (and the below handleContextMessage) ensures that the ES subscription for
|
||||
// all clients includes the minimal required set of events for process tree (NOTIFY_FORK, some EXEC
|
||||
// variant, and NOTIFY_EXIT) but also filters out any events that were subscribed to solely for the
|
||||
// purpose of updating the tree from being processed downstream, where they would be unexpected.
|
||||
- (bool)subscribe:(const std::set<es_event_type_t> &)events {
|
||||
std::set<es_event_type_t> eventsWithLifecycle = events;
|
||||
if (events.find(ES_EVENT_TYPE_NOTIFY_FORK) == events.end()) {
|
||||
eventsWithLifecycle.insert(ES_EVENT_TYPE_NOTIFY_FORK);
|
||||
_addedEvents[ES_EVENT_TYPE_NOTIFY_FORK] = true;
|
||||
}
|
||||
if (events.find(ES_EVENT_TYPE_NOTIFY_EXEC) == events.end() &&
|
||||
events.find(ES_EVENT_TYPE_AUTH_EXEC) == events.end()) {
|
||||
eventsWithLifecycle.insert(ES_EVENT_TYPE_NOTIFY_EXEC);
|
||||
_addedEvents[ES_EVENT_TYPE_NOTIFY_EXEC] = true;
|
||||
}
|
||||
if (events.find(ES_EVENT_TYPE_NOTIFY_EXIT) == events.end()) {
|
||||
eventsWithLifecycle.insert(ES_EVENT_TYPE_NOTIFY_EXIT);
|
||||
_addedEvents[ES_EVENT_TYPE_NOTIFY_EXIT] = true;
|
||||
}
|
||||
|
||||
return [super subscribe:eventsWithLifecycle];
|
||||
}
|
||||
|
||||
- (bool)handleContextMessage:(Message &)esMsg {
|
||||
if (!_processTree) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Inform the tree
|
||||
switch (esMsg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK:
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC:
|
||||
case ES_EVENT_TYPE_AUTH_EXEC:
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT:
|
||||
santa::santad::process_tree::InformFromESEvent(*_processTree, esMsg);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
// Now enumerate the processes that processing this event might require access to...
|
||||
std::vector<struct santa::santad::process_tree::Pid> pids;
|
||||
pids.emplace_back(santa::santad::process_tree::PidFromAuditToken(esMsg->process->audit_token));
|
||||
switch (esMsg->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_EXEC:
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC:
|
||||
pids.emplace_back(
|
||||
santa::santad::process_tree::PidFromAuditToken(esMsg->event.exec.target->audit_token));
|
||||
break;
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK:
|
||||
pids.emplace_back(
|
||||
santa::santad::process_tree::PidFromAuditToken(esMsg->event.fork.child->audit_token));
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
// ...and create the token for those.
|
||||
esMsg.SetProcessToken(santa::santad::process_tree::ProcessToken(_processTree, std::move(pids)));
|
||||
|
||||
return _addedEvents[esMsg->event_type];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -143,11 +143,11 @@ class MockWriter : public Null {
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
{
|
||||
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)));
|
||||
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), 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);
|
||||
@@ -229,10 +229,11 @@ class MockWriter : public Null {
|
||||
EXPECT_CALL(*mockWriter, Write);
|
||||
|
||||
Logger(mockSerializer, mockWriter)
|
||||
.LogFileAccess("v1", "name", Message(mockESApi, &msg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
|
||||
"tgt", FileAccessPolicyDecision::kDenied);
|
||||
.LogFileAccess(
|
||||
"v1", "name", Message(mockESApi, &msg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt), std::nullopt),
|
||||
"tgt", FileAccessPolicyDecision::kDenied);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
|
||||
|
||||
@@ -94,12 +94,14 @@ std::string GetReasonString(SNTEventState event_state) {
|
||||
case SNTEventStateAllowScope: return "SCOPE";
|
||||
case SNTEventStateAllowTeamID: return "TEAMID";
|
||||
case SNTEventStateAllowSigningID: return "SIGNINGID";
|
||||
case SNTEventStateAllowCDHash: return "CDHASH";
|
||||
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 SNTEventStateBlockCDHash: return "CDHASH";
|
||||
case SNTEventStateBlockLongPath: return "LONG_PATH";
|
||||
case SNTEventStateBlockUnknown: return "UNKNOWN";
|
||||
default: return "NOTRUNNING";
|
||||
|
||||
@@ -436,6 +436,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
{SNTEventStateBlockScope, "SCOPE"},
|
||||
{SNTEventStateBlockTeamID, "TEAMID"},
|
||||
{SNTEventStateBlockSigningID, "SIGNINGID"},
|
||||
{SNTEventStateBlockCDHash, "CDHASH"},
|
||||
{SNTEventStateBlockLongPath, "LONG_PATH"},
|
||||
{SNTEventStateAllowUnknown, "UNKNOWN"},
|
||||
{SNTEventStateAllowBinary, "BINARY"},
|
||||
@@ -446,6 +447,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
{SNTEventStateAllowPendingTransitive, "PENDING_TRANSITIVE"},
|
||||
{SNTEventStateAllowTeamID, "TEAMID"},
|
||||
{SNTEventStateAllowSigningID, "SIGNINGID"},
|
||||
{SNTEventStateAllowCDHash, "CDHASH"},
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToReason) {
|
||||
|
||||
@@ -185,6 +185,14 @@ static inline void EncodeFileInfoLight(::pbv1::FileInfoLight *pb_file, const es_
|
||||
pb_file->set_truncated(es_file->path_truncated);
|
||||
}
|
||||
|
||||
static inline void EncodeAnnotations(std::function<::pbv1::process_tree::Annotations *()> lazy_f,
|
||||
const EnrichedProcess &enriched_proc) {
|
||||
if (std::optional<pbv1::process_tree::Annotations> proc_annotations = enriched_proc.annotations();
|
||||
proc_annotations) {
|
||||
*lazy_f() = *proc_annotations;
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeProcessInfoLight(::pbv1::ProcessInfoLight *pb_proc_info,
|
||||
uint32_t message_version, const es_process_t *es_proc,
|
||||
const EnrichedProcess &enriched_proc) {
|
||||
@@ -205,6 +213,8 @@ static inline void EncodeProcessInfoLight(::pbv1::ProcessInfoLight *pb_proc_info
|
||||
enriched_proc.real_group());
|
||||
|
||||
EncodeFileInfoLight(pb_proc_info->mutable_executable(), es_proc->executable);
|
||||
|
||||
EncodeAnnotations([pb_proc_info] { return pb_proc_info->mutable_annotations(); }, enriched_proc);
|
||||
}
|
||||
|
||||
static inline void EncodeProcessInfo(::pbv1::ProcessInfo *pb_proc_info, uint32_t message_version,
|
||||
@@ -256,6 +266,8 @@ static inline void EncodeProcessInfo(::pbv1::ProcessInfo *pb_proc_info, uint32_t
|
||||
if (message_version >= 3) {
|
||||
EncodeTimestamp(pb_proc_info->mutable_start_time(), es_proc->start_time);
|
||||
}
|
||||
|
||||
EncodeAnnotations([pb_proc_info] { return pb_proc_info->mutable_annotations(); }, enriched_proc);
|
||||
}
|
||||
|
||||
void EncodeExitStatus(::pbv1::Exit *pb_exit, int exitStatus) {
|
||||
@@ -299,12 +311,14 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
case SNTEventStateAllowScope: return ::pbv1::Execution::REASON_SCOPE;
|
||||
case SNTEventStateAllowTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
|
||||
case SNTEventStateAllowSigningID: return ::pbv1::Execution::REASON_SIGNING_ID;
|
||||
case SNTEventStateAllowCDHash: return ::pbv1::Execution::REASON_CDHASH;
|
||||
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 SNTEventStateBlockCDHash: return ::pbv1::Execution::REASON_CDHASH;
|
||||
case SNTEventStateBlockLongPath: return ::pbv1::Execution::REASON_LONG_PATH;
|
||||
case SNTEventStateBlockUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
|
||||
default: return ::pbv1::Execution::REASON_NOT_RUNNING;
|
||||
|
||||
@@ -443,6 +443,7 @@ void SerializeAndCheckNonESEvents(
|
||||
{SNTEventStateBlockScope, ::pbv1::Execution::REASON_SCOPE},
|
||||
{SNTEventStateBlockTeamID, ::pbv1::Execution::REASON_TEAM_ID},
|
||||
{SNTEventStateBlockSigningID, ::pbv1::Execution::REASON_SIGNING_ID},
|
||||
{SNTEventStateBlockCDHash, ::pbv1::Execution::REASON_CDHASH},
|
||||
{SNTEventStateBlockLongPath, ::pbv1::Execution::REASON_LONG_PATH},
|
||||
{SNTEventStateAllowUnknown, ::pbv1::Execution::REASON_UNKNOWN},
|
||||
{SNTEventStateAllowBinary, ::pbv1::Execution::REASON_BINARY},
|
||||
@@ -453,6 +454,7 @@ void SerializeAndCheckNonESEvents(
|
||||
{SNTEventStateAllowPendingTransitive, ::pbv1::Execution::REASON_PENDING_TRANSITIVE},
|
||||
{SNTEventStateAllowTeamID, ::pbv1::Execution::REASON_TEAM_ID},
|
||||
{SNTEventStateAllowSigningID, ::pbv1::Execution::REASON_SIGNING_ID},
|
||||
{SNTEventStateAllowCDHash, ::pbv1::Execution::REASON_CDHASH},
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToReason) {
|
||||
|
||||
90
Source/santad/ProcessTree/BUILD
Normal file
90
Source/santad/ProcessTree/BUILD
Normal file
@@ -0,0 +1,90 @@
|
||||
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
|
||||
load("//:helper.bzl", "santa_unit_test")
|
||||
|
||||
cc_library(
|
||||
name = "process",
|
||||
hdrs = ["process.h"],
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [
|
||||
"//Source/santad/ProcessTree/annotations:annotator",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "process_tree",
|
||||
srcs = [
|
||||
"process_tree.cc",
|
||||
"process_tree_macos.mm",
|
||||
],
|
||||
hdrs = [
|
||||
"process_tree.h",
|
||||
"process_tree_macos.h",
|
||||
],
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [
|
||||
":process",
|
||||
"//Source/santad/ProcessTree:process_tree_cc_proto",
|
||||
"//Source/santad/ProcessTree/annotations:annotator",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
"@com_google_absl//absl/container:flat_hash_set",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
)
|
||||
|
||||
proto_library(
|
||||
name = "process_tree_proto",
|
||||
srcs = ["process_tree.proto"],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "process_tree_cc_proto",
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [":process_tree_proto"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTEndpointSecurityAdapter",
|
||||
srcs = ["SNTEndpointSecurityAdapter.mm"],
|
||||
hdrs = ["SNTEndpointSecurityAdapter.h"],
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [
|
||||
":process_tree",
|
||||
"//Source/santad:EndpointSecurityAPI",
|
||||
"//Source/santad:EndpointSecurityMessage",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "process_tree_test_helpers",
|
||||
srcs = ["process_tree_test_helpers.mm"],
|
||||
hdrs = ["process_tree_test_helpers.h"],
|
||||
deps = [
|
||||
":process",
|
||||
":process_tree",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "process_tree_test",
|
||||
srcs = ["process_tree_test.mm"],
|
||||
deps = [
|
||||
":process",
|
||||
":process_tree_test_helpers",
|
||||
"//Source/santad/ProcessTree/annotations:annotator",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
)
|
||||
33
Source/santad/ProcessTree/SNTEndpointSecurityAdapter.h
Normal file
33
Source/santad/ProcessTree/SNTEndpointSecurityAdapter.h
Normal file
@@ -0,0 +1,33 @@
|
||||
/// 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__SANTAD_PROCESSTREE_SNTENDPOINTSECURITYADAPTER_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_SNTENDPOINTSECURITYADAPTER_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
// Inform the tree of the ES event in msg.
|
||||
// This is idempotent on the tree, so can be called from multiple places with
|
||||
// the same msg.
|
||||
void InformFromESEvent(
|
||||
ProcessTree &tree,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg);
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
73
Source/santad/ProcessTree/SNTEndpointSecurityAdapter.mm
Normal file
73
Source/santad/ProcessTree/SNTEndpointSecurityAdapter.mm
Normal file
@@ -0,0 +1,73 @@
|
||||
/// 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.
|
||||
#include "Source/santad/ProcessTree/SNTEndpointSecurityAdapter.h"
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <Foundation/Foundation.h>
|
||||
#include <bsm/libbsm.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
#include "Source/santad/ProcessTree/process_tree_macos.h"
|
||||
#include "absl/status/statusor.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
void InformFromESEvent(ProcessTree &tree, const Message &msg) {
|
||||
struct Pid event_pid = PidFromAuditToken(msg->process->audit_token);
|
||||
auto proc = tree.Get(event_pid);
|
||||
|
||||
if (!proc) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<EndpointSecurityAPI> esapi = msg.ESAPI();
|
||||
|
||||
switch (msg->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_EXEC:
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC: {
|
||||
std::vector<std::string> args;
|
||||
args.reserve(esapi->ExecArgCount(&msg->event.exec));
|
||||
for (int i = 0; i < esapi->ExecArgCount(&msg->event.exec); i++) {
|
||||
es_string_token_t arg = esapi->ExecArg(&msg->event.exec, i);
|
||||
args.push_back(std::string(arg.data, arg.length));
|
||||
}
|
||||
|
||||
es_string_token_t executable = msg->event.exec.target->executable->path;
|
||||
tree.HandleExec(
|
||||
msg->mach_time, **proc, PidFromAuditToken(msg->event.exec.target->audit_token),
|
||||
(struct Program){.executable = std::string(executable.data, executable.length),
|
||||
.arguments = args},
|
||||
(struct Cred){
|
||||
.uid = audit_token_to_euid(msg->event.exec.target->audit_token),
|
||||
.gid = audit_token_to_egid(msg->event.exec.target->audit_token),
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK: {
|
||||
tree.HandleFork(msg->mach_time, **proc,
|
||||
PidFromAuditToken(msg->event.fork.child->audit_token));
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT: tree.HandleExit(msg->mach_time, **proc); break;
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
11
Source/santad/ProcessTree/annotations/BUILD
Normal file
11
Source/santad/ProcessTree/annotations/BUILD
Normal file
@@ -0,0 +1,11 @@
|
||||
package(
|
||||
default_visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "annotator",
|
||||
hdrs = ["annotator.h"],
|
||||
deps = [
|
||||
"//Source/santad/ProcessTree:process_tree_cc_proto",
|
||||
],
|
||||
)
|
||||
40
Source/santad/ProcessTree/annotations/annotator.h
Normal file
40
Source/santad/ProcessTree/annotations/annotator.h
Normal file
@@ -0,0 +1,40 @@
|
||||
/// 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__SANTAD_PROCESSTREE_ANNOTATIONS_BASE_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_ANNOTATIONS_BASE_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "Source/santad/ProcessTree/process_tree.pb.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
class ProcessTree;
|
||||
class Process;
|
||||
|
||||
class Annotator {
|
||||
public:
|
||||
virtual ~Annotator() = default;
|
||||
|
||||
virtual void AnnotateFork(ProcessTree &tree, const Process &parent,
|
||||
const Process &child) = 0;
|
||||
virtual void AnnotateExec(ProcessTree &tree, const Process &orig_process,
|
||||
const Process &new_process) = 0;
|
||||
virtual std::optional<::santa::pb::v1::process_tree::Annotations> Proto()
|
||||
const = 0;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
114
Source/santad/ProcessTree/process.h
Normal file
114
Source/santad/ProcessTree/process.h
Normal file
@@ -0,0 +1,114 @@
|
||||
/// 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__SANTAD_PROCESSTREE_PROCESS_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_PROCESS_H
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <typeindex>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/ProcessTree/annotations/annotator.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
struct Pid {
|
||||
pid_t pid;
|
||||
uint64_t pidversion;
|
||||
|
||||
friend bool operator==(const struct Pid &lhs, const struct Pid &rhs) {
|
||||
return lhs.pid == rhs.pid && lhs.pidversion == rhs.pidversion;
|
||||
}
|
||||
friend bool operator!=(const struct Pid &lhs, const struct Pid &rhs) {
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename H>
|
||||
H AbslHashValue(H h, const struct Pid &p) {
|
||||
return H::combine(std::move(h), p.pid, p.pidversion);
|
||||
}
|
||||
|
||||
struct Cred {
|
||||
uid_t uid;
|
||||
gid_t gid;
|
||||
|
||||
friend bool operator==(const struct Cred &lhs, const struct Cred &rhs) {
|
||||
return lhs.uid == rhs.uid && lhs.gid == rhs.gid;
|
||||
}
|
||||
friend bool operator!=(const struct Cred &lhs, const struct Cred &rhs) {
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
};
|
||||
|
||||
struct Program {
|
||||
std::string executable;
|
||||
std::vector<std::string> arguments;
|
||||
|
||||
friend bool operator==(const struct Program &lhs, const struct Program &rhs) {
|
||||
return lhs.executable == rhs.executable && lhs.arguments == rhs.arguments;
|
||||
}
|
||||
friend bool operator!=(const struct Program &lhs, const struct Program &rhs) {
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
};
|
||||
|
||||
// Fwd decls
|
||||
class ProcessTree;
|
||||
|
||||
class Process {
|
||||
public:
|
||||
explicit Process(const Pid pid, const Cred cred,
|
||||
std::shared_ptr<const Program> program,
|
||||
std::shared_ptr<const Process> parent)
|
||||
: pid_(pid),
|
||||
effective_cred_(cred),
|
||||
program_(program),
|
||||
annotations_(),
|
||||
parent_(parent),
|
||||
refcnt_(0),
|
||||
tombstoned_(false) {}
|
||||
Process(const Process &) = default;
|
||||
Process &operator=(const Process &) = delete;
|
||||
Process(Process &&) = default;
|
||||
Process &operator=(Process &&) = delete;
|
||||
|
||||
// Const "attributes" are public
|
||||
const struct Pid pid_;
|
||||
const struct Cred effective_cred_;
|
||||
const std::shared_ptr<const Program> program_;
|
||||
|
||||
private:
|
||||
// This is not API.
|
||||
// The tree helper methods are the API, and we just happen to implement
|
||||
// annotation storage and the parent relation in memory on the process right
|
||||
// now.
|
||||
friend class ProcessTree;
|
||||
absl::flat_hash_map<std::type_index, std::shared_ptr<const Annotator>>
|
||||
annotations_;
|
||||
std::shared_ptr<const Process> parent_;
|
||||
// TODO(nickmg): atomic here breaks the build.
|
||||
int refcnt_;
|
||||
// If the process is tombstoned, the event removing it from the tree has been
|
||||
// processed, but refcnt>0 keeps it alive.
|
||||
bool tombstoned_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
316
Source/santad/ProcessTree/process_tree.cc
Normal file
316
Source/santad/ProcessTree/process_tree.cc
Normal file
@@ -0,0 +1,316 @@
|
||||
/// 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.
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <typeindex>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/ProcessTree/annotations/annotator.h"
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.pb.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
void ProcessTree::BackfillInsertChildren(
|
||||
absl::flat_hash_map<pid_t, std::vector<Process>> &parent_map,
|
||||
std::shared_ptr<Process> parent, const Process &unlinked_proc) {
|
||||
auto proc = std::make_shared<Process>(
|
||||
unlinked_proc.pid_, unlinked_proc.effective_cred_,
|
||||
// Re-use shared pointers from parent if value equivalent
|
||||
(parent && *(unlinked_proc.program_) == *(parent->program_))
|
||||
? parent->program_
|
||||
: unlinked_proc.program_,
|
||||
parent);
|
||||
{
|
||||
absl::MutexLock lock(&mtx_);
|
||||
map_.emplace(unlinked_proc.pid_, proc);
|
||||
}
|
||||
|
||||
// The only case where we should not have a parent is the root processes
|
||||
// (e.g. init, kthreadd).
|
||||
if (parent) {
|
||||
for (auto &annotator : annotators_) {
|
||||
annotator->AnnotateFork(*this, *(proc->parent_), *proc);
|
||||
if (proc->program_ != proc->parent_->program_) {
|
||||
annotator->AnnotateExec(*this, *(proc->parent_), *proc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const Process &child : parent_map[unlinked_proc.pid_.pid]) {
|
||||
BackfillInsertChildren(parent_map, proc, child);
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessTree::HandleFork(uint64_t timestamp, const Process &parent,
|
||||
const Pid new_pid) {
|
||||
if (Step(timestamp)) {
|
||||
std::shared_ptr<Process> child;
|
||||
{
|
||||
absl::MutexLock lock(&mtx_);
|
||||
child = std::make_shared<Process>(new_pid, parent.effective_cred_,
|
||||
parent.program_, map_[parent.pid_]);
|
||||
map_.emplace(new_pid, child);
|
||||
}
|
||||
for (const auto &annotator : annotators_) {
|
||||
annotator->AnnotateFork(*this, parent, *child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessTree::HandleExec(uint64_t timestamp, const Process &p,
|
||||
const Pid new_pid, const Program prog,
|
||||
const Cred c) {
|
||||
if (Step(timestamp)) {
|
||||
// TODO(nickmg): should struct pid be reworked and only pid_version be
|
||||
// passed?
|
||||
assert(new_pid.pid == p.pid_.pid);
|
||||
|
||||
auto new_proc = std::make_shared<Process>(
|
||||
new_pid, c, std::make_shared<const Program>(prog), p.parent_);
|
||||
{
|
||||
absl::MutexLock lock(&mtx_);
|
||||
remove_at_.push_back({timestamp, p.pid_});
|
||||
map_.emplace(new_proc->pid_, new_proc);
|
||||
}
|
||||
for (const auto &annotator : annotators_) {
|
||||
annotator->AnnotateExec(*this, p, *new_proc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessTree::HandleExit(uint64_t timestamp, const Process &p) {
|
||||
if (Step(timestamp)) {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
remove_at_.push_back({timestamp, p.pid_});
|
||||
}
|
||||
}
|
||||
|
||||
bool ProcessTree::Step(uint64_t timestamp) {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
uint64_t new_cutoff = seen_timestamps_.front();
|
||||
if (timestamp < new_cutoff) {
|
||||
// Event timestamp is before the rolling list of seen events.
|
||||
// This event may or may not have been processed, but be conservative and
|
||||
// do not reprocess.
|
||||
return false;
|
||||
}
|
||||
|
||||
// seen_timestamps_ is sorted, so only look for the value if it's possibly
|
||||
// within the array.
|
||||
if (timestamp < seen_timestamps_.back()) {
|
||||
// TODO(nickmg): If array is made bigger, replace with a binary search.
|
||||
for (const auto seen_ts : seen_timestamps_) {
|
||||
if (seen_ts == timestamp) {
|
||||
// Event seen, signal it should not be reprocessed.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto insert_point =
|
||||
std::find_if(seen_timestamps_.rbegin(), seen_timestamps_.rend(),
|
||||
[&](uint64_t x) { return x < timestamp; });
|
||||
std::move(seen_timestamps_.begin() + 1, insert_point.base(),
|
||||
seen_timestamps_.begin());
|
||||
*insert_point = timestamp;
|
||||
|
||||
for (auto it = remove_at_.begin(); it != remove_at_.end();) {
|
||||
if (it->first < new_cutoff) {
|
||||
if (auto target = GetLocked(it->second);
|
||||
target && (*target)->refcnt_ > 0) {
|
||||
(*target)->tombstoned_ = true;
|
||||
} else {
|
||||
map_.erase(it->second);
|
||||
}
|
||||
it = remove_at_.erase(it);
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProcessTree::RetainProcess(std::vector<struct Pid> &pids) {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
for (const struct Pid &p : pids) {
|
||||
auto proc = GetLocked(p);
|
||||
if (proc) {
|
||||
(*proc)->refcnt_++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessTree::ReleaseProcess(std::vector<struct Pid> &pids) {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
for (const struct Pid &p : pids) {
|
||||
auto proc = GetLocked(p);
|
||||
if (proc) {
|
||||
if (--(*proc)->refcnt_ == 0 && (*proc)->tombstoned_) {
|
||||
map_.erase(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
---
|
||||
Annotation get/set
|
||||
---
|
||||
*/
|
||||
|
||||
void ProcessTree::AnnotateProcess(const Process &p,
|
||||
std::shared_ptr<const Annotator> a) {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
const Annotator &x = *a;
|
||||
map_[p.pid_]->annotations_.emplace(std::type_index(typeid(x)), std::move(a));
|
||||
}
|
||||
|
||||
std::optional<::santa::pb::v1::process_tree::Annotations>
|
||||
ProcessTree::ExportAnnotations(const Pid p) {
|
||||
auto proc = Get(p);
|
||||
if (!proc || (*proc)->annotations_.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
::santa::pb::v1::process_tree::Annotations a;
|
||||
for (const auto &[_, annotation] : (*proc)->annotations_) {
|
||||
if (auto x = annotation->Proto(); x) a.MergeFrom(*x);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
/*
|
||||
---
|
||||
Tree inspection methods
|
||||
---
|
||||
*/
|
||||
|
||||
std::vector<std::shared_ptr<const Process>> ProcessTree::RootSlice(
|
||||
std::shared_ptr<const Process> p) const {
|
||||
std::vector<std::shared_ptr<const Process>> slice;
|
||||
while (p) {
|
||||
slice.push_back(p);
|
||||
p = p->parent_;
|
||||
}
|
||||
return slice;
|
||||
}
|
||||
|
||||
void ProcessTree::Iterate(
|
||||
std::function<void(std::shared_ptr<const Process> p)> f) const {
|
||||
std::vector<std::shared_ptr<const Process>> procs;
|
||||
{
|
||||
absl::ReaderMutexLock lock(&mtx_);
|
||||
procs.reserve(map_.size());
|
||||
for (auto &[_, proc] : map_) {
|
||||
procs.push_back(proc);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &p : procs) {
|
||||
f(p);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::shared_ptr<const Process>> ProcessTree::Get(
|
||||
const Pid target) const {
|
||||
absl::ReaderMutexLock lock(&mtx_);
|
||||
return GetLocked(target);
|
||||
}
|
||||
|
||||
std::optional<std::shared_ptr<Process>> ProcessTree::GetLocked(
|
||||
const Pid target) const {
|
||||
auto it = map_.find(target);
|
||||
if (it == map_.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::shared_ptr<const Process> ProcessTree::GetParent(const Process &p) const {
|
||||
return p.parent_;
|
||||
}
|
||||
|
||||
#if SANTA_PROCESS_TREE_DEBUG
|
||||
void ProcessTree::DebugDump(std::ostream &stream) const {
|
||||
absl::ReaderMutexLock lock(&mtx_);
|
||||
stream << map_.size() << " processes" << std::endl;
|
||||
DebugDumpLocked(stream, 0, 0);
|
||||
}
|
||||
|
||||
void ProcessTree::DebugDumpLocked(std::ostream &stream, int depth,
|
||||
pid_t ppid) const
|
||||
ABSL_SHARED_LOCKS_REQUIRED(mtx_) {
|
||||
for (auto &[_, process] : map_) {
|
||||
if ((ppid == 0 && !process->parent_) ||
|
||||
(process->parent_ && process->parent_->pid_.pid == ppid)) {
|
||||
stream << std::string(2 * depth, ' ') << process->pid_.pid
|
||||
<< process->program_->executable << std::endl;
|
||||
DebugDumpLocked(stream, depth + 1, process->pid_.pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
absl::StatusOr<std::shared_ptr<ProcessTree>> CreateTree(
|
||||
std::vector<std::unique_ptr<Annotator>> annotations) {
|
||||
absl::flat_hash_set<std::type_index> seen;
|
||||
for (const auto &annotator : annotations) {
|
||||
if (seen.count(std::type_index(typeid(annotator)))) {
|
||||
return absl::InvalidArgumentError(
|
||||
"Multiple annotators of the same class");
|
||||
}
|
||||
seen.emplace(std::type_index(typeid(annotator)));
|
||||
}
|
||||
|
||||
if (seen.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto tree = std::make_shared<ProcessTree>(std::move(annotations));
|
||||
if (auto status = tree->Backfill(); !status.ok()) {
|
||||
return status;
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
/*
|
||||
----
|
||||
Tokens
|
||||
----
|
||||
*/
|
||||
|
||||
ProcessToken::ProcessToken(std::shared_ptr<ProcessTree> tree,
|
||||
std::vector<struct Pid> pids)
|
||||
: tree_(std::move(tree)), pids_(std::move(pids)) {
|
||||
tree_->RetainProcess(pids);
|
||||
}
|
||||
|
||||
ProcessToken::~ProcessToken() { tree_->ReleaseProcess(pids_); }
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
189
Source/santad/ProcessTree/process_tree.h
Normal file
189
Source/santad/ProcessTree/process_tree.h
Normal file
@@ -0,0 +1,189 @@
|
||||
/// 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__SANTAD_PROCESSTREE_TREE_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_TREE_H
|
||||
|
||||
#include <memory>
|
||||
#include <typeinfo>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
absl::StatusOr<Process> LoadPID(pid_t pid);
|
||||
|
||||
// Fwd decl for test peer.
|
||||
class ProcessTreeTestPeer;
|
||||
|
||||
class ProcessTree {
|
||||
public:
|
||||
explicit ProcessTree(std::vector<std::unique_ptr<Annotator>> &&annotators)
|
||||
: annotators_(std::move(annotators)), seen_timestamps_({}) {}
|
||||
ProcessTree(const ProcessTree &) = delete;
|
||||
ProcessTree &operator=(const ProcessTree &) = delete;
|
||||
ProcessTree(ProcessTree &&) = delete;
|
||||
ProcessTree &operator=(ProcessTree &&) = delete;
|
||||
|
||||
// Initialize the tree with the processes currently running on the system.
|
||||
absl::Status Backfill();
|
||||
|
||||
// Inform the tree of a fork event, in which the parent process spawns a child
|
||||
// with the only difference between the two being the pid.
|
||||
void HandleFork(uint64_t timestamp, const Process &parent,
|
||||
struct Pid new_pid);
|
||||
|
||||
// Inform the tree of an exec event, in which the program and potentially cred
|
||||
// of a Process change.
|
||||
// p is the process performing the exec (running the "old" program),
|
||||
// and new_pid, prog, and cred are the new pid, program, and credentials
|
||||
// after the exec.
|
||||
// N.B. new_pid is required as the "pid version" will have changed.
|
||||
// It is a programming error to pass a new_pid such that
|
||||
// p.pid_.pid != new_pid.pid.
|
||||
void HandleExec(uint64_t timestamp, const Process &p, struct Pid new_pid,
|
||||
struct Program prog, struct Cred c);
|
||||
|
||||
// Inform the tree of a process exit.
|
||||
void HandleExit(uint64_t timestamp, const Process &p);
|
||||
|
||||
// Mark the given pids as needing to be retained in the tree's map for future
|
||||
// access. Normally, Processes are removed once all clients process past the
|
||||
// event which would remove the Process (e.g. exit), however in cases where
|
||||
// async processing occurs, the Process may need to be accessed after the
|
||||
// exit.
|
||||
void RetainProcess(std::vector<struct Pid> &pids);
|
||||
|
||||
// Release previously retained processes, signaling that the client is done
|
||||
// processing the event that retained them.
|
||||
void ReleaseProcess(std::vector<struct Pid> &pids);
|
||||
|
||||
// Annotate the given process with an Annotator (state).
|
||||
void AnnotateProcess(const Process &p, std::shared_ptr<const Annotator> a);
|
||||
|
||||
// Get the given annotation on the given process if it exists, or nullopt if
|
||||
// the annotation is not set.
|
||||
template <typename T>
|
||||
std::optional<std::shared_ptr<const T>> GetAnnotation(const Process &p) const;
|
||||
|
||||
// Get the fully merged proto form of all annotations on the given process.
|
||||
std::optional<::santa::pb::v1::process_tree::Annotations> ExportAnnotations(
|
||||
struct Pid p);
|
||||
|
||||
// Atomically get the slice of Processes going from the given process "up"
|
||||
// to the root. The root process has no parent. N.B. There may be more than
|
||||
// one root process. E.g. on Linux, both init (PID 1) and kthread (PID 2)
|
||||
// are considered roots, as they are reported to have PPID=0.
|
||||
std::vector<std::shared_ptr<const Process>> RootSlice(
|
||||
std::shared_ptr<const Process> p) const;
|
||||
|
||||
// Call f for all processes in the tree. The list of processes is captured
|
||||
// before invoking f, so it is safe to mutate the tree in f.
|
||||
void Iterate(std::function<void(std::shared_ptr<const Process>)> f) const;
|
||||
|
||||
// Get the Process for the given pid in the tree if it exists.
|
||||
std::optional<std::shared_ptr<const Process>> Get(struct Pid target) const;
|
||||
|
||||
// Traverse the tree from the given Process to its parent.
|
||||
std::shared_ptr<const Process> GetParent(const Process &p) const;
|
||||
|
||||
#if SANTA_PROCESS_TREE_DEBUG
|
||||
// Dump the tree in a human readable form to the given ostream.
|
||||
void DebugDump(std::ostream &stream) const;
|
||||
#endif
|
||||
|
||||
private:
|
||||
friend class ProcessTreeTestPeer;
|
||||
void BackfillInsertChildren(
|
||||
absl::flat_hash_map<pid_t, std::vector<Process>> &parent_map,
|
||||
std::shared_ptr<Process> parent, const Process &unlinked_proc);
|
||||
|
||||
// Mark that an event with the given timestamp is being processed.
|
||||
// Returns whether the given timestamp is "novel", and the tree should be
|
||||
// updated with the results of the event.
|
||||
bool Step(uint64_t timestamp);
|
||||
|
||||
std::optional<std::shared_ptr<Process>> GetLocked(struct Pid target) const
|
||||
ABSL_SHARED_LOCKS_REQUIRED(mtx_);
|
||||
|
||||
void DebugDumpLocked(std::ostream &stream, int depth, pid_t ppid) const;
|
||||
|
||||
std::vector<std::unique_ptr<Annotator>> annotators_;
|
||||
|
||||
mutable absl::Mutex mtx_;
|
||||
absl::flat_hash_map<const struct Pid, std::shared_ptr<Process>> map_
|
||||
ABSL_GUARDED_BY(mtx_);
|
||||
// List of pids which should be removed from map_, and at the timestamp at
|
||||
// which they should be.
|
||||
// Elements are removed when the timestamp falls out of the seen_timestamps_
|
||||
// list below, signifying that all clients have synced past the timestamp.
|
||||
std::vector<std::pair<uint64_t, struct Pid>> remove_at_ ABSL_GUARDED_BY(mtx_);
|
||||
// Rolling list of event timestamps processed by the tree.
|
||||
// This is used to ensure an event only gets processed once, even if events
|
||||
// come out of order.
|
||||
std::array<uint64_t, 32> seen_timestamps_ ABSL_GUARDED_BY(mtx_);
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
std::optional<std::shared_ptr<const T>> ProcessTree::GetAnnotation(
|
||||
const Process &p) const {
|
||||
auto it = p.annotations_.find(std::type_index(typeid(T)));
|
||||
if (it == p.annotations_.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::dynamic_pointer_cast<const T>(it->second);
|
||||
}
|
||||
|
||||
// Create a new tree, ensuring the provided annotations are valid and that
|
||||
// backfill is successful.
|
||||
absl::StatusOr<std::shared_ptr<ProcessTree>> CreateTree(
|
||||
std::vector<std::unique_ptr<Annotator>> annotations);
|
||||
|
||||
// ProcessTokens provide a lifetime based approach to retaining processes
|
||||
// in a ProcessTree. When a token is created with a list of pids that may need
|
||||
// to be referenced during processing of a given event, the ProcessToken informs
|
||||
// the tree to retain those pids in its map so any call to ProcessTree::Get()
|
||||
// during event processing succeeds. When the token is destroyed, it signals the
|
||||
// tree to release the pids, which removes them from the tree if they would have
|
||||
// fallen out otherwise due to a destruction event (e.g. exit).
|
||||
class ProcessToken {
|
||||
public:
|
||||
explicit ProcessToken(std::shared_ptr<ProcessTree> tree,
|
||||
std::vector<struct Pid> pids);
|
||||
~ProcessToken();
|
||||
ProcessToken(const ProcessToken &other)
|
||||
: ProcessToken(other.tree_, other.pids_) {}
|
||||
ProcessToken(ProcessToken &&other) noexcept
|
||||
: tree_(std::move(other.tree_)), pids_(std::move(other.pids_)) {}
|
||||
ProcessToken &operator=(const ProcessToken &other) {
|
||||
return *this = ProcessToken(other.tree_, other.pids_);
|
||||
}
|
||||
ProcessToken &operator=(ProcessToken &&other) noexcept {
|
||||
tree_ = std::move(other.tree_);
|
||||
pids_ = std::move(other.pids_);
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<ProcessTree> tree_;
|
||||
std::vector<struct Pid> pids_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
6
Source/santad/ProcessTree/process_tree.proto
Normal file
6
Source/santad/ProcessTree/process_tree.proto
Normal file
@@ -0,0 +1,6 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package santa.pb.v1.process_tree;
|
||||
|
||||
message Annotations {
|
||||
}
|
||||
26
Source/santad/ProcessTree/process_tree_macos.h
Normal file
26
Source/santad/ProcessTree/process_tree_macos.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/// 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__SANTAD_PROCESSTREE_TREE_MACOS_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_TREE_MACOS_H
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
// Create a struct pid from the given audit token.
|
||||
struct Pid PidFromAuditToken(const audit_token_t &tok);
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
185
Source/santad/ProcessTree/process_tree_macos.mm
Normal file
185
Source/santad/ProcessTree/process_tree_macos.mm
Normal file
@@ -0,0 +1,185 @@
|
||||
/// 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.
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <libproc.h>
|
||||
#include <mach/message.h>
|
||||
#include <string.h>
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
namespace {
|
||||
// Modified from
|
||||
// https://chromium.googlesource.com/crashpad/crashpad/+/360e441c53ab4191a6fd2472cc57c3343a2f6944/util/posix/process_util_mac.cc
|
||||
// TODO: https://github.com/apple-oss-distributions/adv_cmds/blob/main/ps/ps.c
|
||||
absl::StatusOr<std::vector<std::string>> ProcessArgumentsForPID(pid_t pid) {
|
||||
// The format of KERN_PROCARGS2 is explained in 10.9.2 adv_cmds-153/ps/print.c
|
||||
// getproclline(). It is an int (argc) followed by the executable’s string
|
||||
// area. The string area consists of NUL-terminated strings, beginning with
|
||||
// the executable path, and then starting on an aligned boundary, all of the
|
||||
// elements of argv, envp, and applev.
|
||||
// It is possible for a process to exec() in between the two sysctl() calls
|
||||
// below. If that happens, and the string area of the new program is larger
|
||||
// than that of the old one, args_size_estimate will be too small. To detect
|
||||
// this situation, the second sysctl() attempts to fetch args_size_estimate +
|
||||
// 1 bytes, expecting to only receive args_size_estimate. If it gets the extra
|
||||
// byte, it indicates that the string area has grown, and the sysctl() pair
|
||||
// will be retried a limited number of times.
|
||||
size_t args_size_estimate;
|
||||
size_t args_size;
|
||||
std::string args;
|
||||
int tries = 3;
|
||||
do {
|
||||
int mib[] = {CTL_KERN, KERN_PROCARGS2, pid};
|
||||
int rv = sysctl(mib, 3, nullptr, &args_size_estimate, nullptr, 0);
|
||||
if (rv != 0) {
|
||||
return absl::InternalError("KERN_PROCARGS2");
|
||||
}
|
||||
args_size = args_size_estimate + 1;
|
||||
args.resize(args_size);
|
||||
rv = sysctl(mib, 3, &args[0], &args_size, nullptr, 0);
|
||||
if (rv != 0) {
|
||||
return absl::InternalError("KERN_PROCARGS2");
|
||||
}
|
||||
} while (args_size == args_size_estimate + 1 && tries--);
|
||||
if (args_size == args_size_estimate + 1) {
|
||||
return absl::InternalError("Couldn't determine size");
|
||||
}
|
||||
// KERN_PROCARGS2 needs to at least contain argc.
|
||||
if (args_size < sizeof(int)) {
|
||||
return absl::InternalError("Bad args_size");
|
||||
}
|
||||
args.resize(args_size);
|
||||
// Get argc.
|
||||
int argc;
|
||||
memcpy(&argc, &args[0], sizeof(argc));
|
||||
// Find the end of the executable path.
|
||||
size_t start_pos = sizeof(argc);
|
||||
size_t nul_pos = args.find('\0', start_pos);
|
||||
if (nul_pos == std::string::npos) {
|
||||
return absl::InternalError("Can't find end of executable path");
|
||||
}
|
||||
// Find the beginning of the string area.
|
||||
start_pos = args.find_first_not_of('\0', nul_pos);
|
||||
if (start_pos == std::string::npos) {
|
||||
return absl::InternalError("Can't find args after executable path");
|
||||
}
|
||||
std::vector<std::string> local_argv;
|
||||
while (argc-- && nul_pos != std::string::npos) {
|
||||
nul_pos = args.find('\0', start_pos);
|
||||
local_argv.push_back(args.substr(start_pos, nul_pos - start_pos));
|
||||
start_pos = nul_pos + 1;
|
||||
}
|
||||
return local_argv;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
struct Pid PidFromAuditToken(const audit_token_t &tok) {
|
||||
return (struct Pid){.pid = audit_token_to_pid(tok),
|
||||
.pidversion = (uint64_t)audit_token_to_pidversion(tok)};
|
||||
}
|
||||
|
||||
absl::StatusOr<Process> LoadPID(pid_t pid) {
|
||||
task_name_t task;
|
||||
mach_msg_type_number_t size = TASK_AUDIT_TOKEN_COUNT;
|
||||
audit_token_t token;
|
||||
|
||||
if (task_name_for_pid(mach_task_self(), pid, &task) != KERN_SUCCESS) {
|
||||
return absl::InternalError("task_name_for_pid");
|
||||
}
|
||||
|
||||
if (task_info(task, TASK_AUDIT_TOKEN, (task_info_t)&token, &size) != KERN_SUCCESS) {
|
||||
return absl::InternalError("task_info(TASK_AUDIT_TOKEN)");
|
||||
}
|
||||
mach_port_deallocate(mach_task_self(), task);
|
||||
|
||||
char path[PROC_PIDPATHINFO_MAXSIZE];
|
||||
if (proc_pidpath_audittoken(&token, path, sizeof(path)) <= 0) {
|
||||
return absl::InternalError("proc_pidpath_audittoken");
|
||||
}
|
||||
|
||||
// Don't fail Process creation if args can't be recovered.
|
||||
std::vector<std::string> args =
|
||||
ProcessArgumentsForPID(audit_token_to_pid(token)).value_or(std::vector<std::string>());
|
||||
|
||||
return Process((struct Pid){.pid = audit_token_to_pid(token),
|
||||
.pidversion = (uint64_t)audit_token_to_pidversion(token)},
|
||||
(struct Cred){
|
||||
.uid = audit_token_to_euid(token),
|
||||
.gid = audit_token_to_egid(token),
|
||||
},
|
||||
std::make_shared<struct Program>((struct Program){
|
||||
.executable = path,
|
||||
.arguments = args,
|
||||
}),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
absl::Status ProcessTree::Backfill() {
|
||||
int n_procs = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0);
|
||||
if (n_procs < 0) {
|
||||
return absl::InternalError("proc_listpids failed");
|
||||
}
|
||||
n_procs /= sizeof(pid_t);
|
||||
|
||||
std::vector<pid_t> pids;
|
||||
pids.resize(n_procs + 16); // add space for a few more processes
|
||||
// in case some spawn in-between.
|
||||
|
||||
n_procs = proc_listpids(PROC_ALL_PIDS, 0, pids.data(), (int)(pids.size() * sizeof(pid_t)));
|
||||
if (n_procs < 0) {
|
||||
return absl::InternalError("proc_listpids failed");
|
||||
}
|
||||
n_procs /= sizeof(pid_t);
|
||||
pids.resize(n_procs);
|
||||
|
||||
absl::flat_hash_map<pid_t, std::vector<Process>> parent_map;
|
||||
for (pid_t pid : pids) {
|
||||
auto proc_status = LoadPID(pid);
|
||||
if (proc_status.ok()) {
|
||||
auto unlinked_proc = proc_status.value();
|
||||
|
||||
// Determine ppid
|
||||
// Alternatively, there's a sysctl interface:
|
||||
// https://chromium.googlesource.com/chromium/chromium/+/master/base/process_util_openbsd.cc#32
|
||||
struct proc_bsdinfo bsdinfo;
|
||||
if (proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &bsdinfo, sizeof(bsdinfo)) !=
|
||||
PROC_PIDTBSDINFO_SIZE) {
|
||||
continue;
|
||||
};
|
||||
|
||||
parent_map[bsdinfo.pbi_ppid].push_back(unlinked_proc);
|
||||
}
|
||||
}
|
||||
|
||||
auto &roots = parent_map[0];
|
||||
for (const Process &p : roots) {
|
||||
BackfillInsertChildren(parent_map, std::shared_ptr<Process>(), p);
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
246
Source/santad/ProcessTree/process_tree_test.mm
Normal file
246
Source/santad/ProcessTree/process_tree_test.mm
Normal file
@@ -0,0 +1,246 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "Source/santad/ProcessTree/annotations/annotator.h"
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "Source/santad/ProcessTree/process_tree_test_helpers.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
|
||||
namespace ptpb = ::santa::pb::v1::process_tree;
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
static constexpr std::string_view kAnnotatedExecutable = "/usr/bin/login";
|
||||
|
||||
class TestAnnotator : public Annotator {
|
||||
public:
|
||||
TestAnnotator() {}
|
||||
void AnnotateFork(ProcessTree &tree, const Process &parent, const Process &child) override;
|
||||
void AnnotateExec(ProcessTree &tree, const Process &orig_process,
|
||||
const Process &new_process) override;
|
||||
std::optional<::ptpb::Annotations> Proto() const override;
|
||||
};
|
||||
|
||||
void TestAnnotator::AnnotateFork(ProcessTree &tree, const Process &parent, const Process &child) {
|
||||
// "Base case". Propagate existing annotations down to descendants.
|
||||
if (auto annotation = tree.GetAnnotation<TestAnnotator>(parent)) {
|
||||
tree.AnnotateProcess(child, std::move(*annotation));
|
||||
}
|
||||
}
|
||||
|
||||
void TestAnnotator::AnnotateExec(ProcessTree &tree, const Process &orig_process,
|
||||
const Process &new_process) {
|
||||
if (auto annotation = tree.GetAnnotation<TestAnnotator>(orig_process)) {
|
||||
tree.AnnotateProcess(new_process, std::move(*annotation));
|
||||
return;
|
||||
}
|
||||
|
||||
if (new_process.program_->executable == kAnnotatedExecutable) {
|
||||
tree.AnnotateProcess(new_process, std::make_shared<TestAnnotator>());
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<::ptpb::Annotations> TestAnnotator::Proto() const {
|
||||
return std::nullopt;
|
||||
}
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
using namespace santa::santad::process_tree;
|
||||
|
||||
@interface ProcessTreeTest : XCTestCase
|
||||
@property std::shared_ptr<ProcessTreeTestPeer> tree;
|
||||
@property std::shared_ptr<const Process> initProc;
|
||||
@end
|
||||
|
||||
@implementation ProcessTreeTest
|
||||
|
||||
- (void)setUp {
|
||||
std::vector<std::unique_ptr<Annotator>> annotators{};
|
||||
self.tree = std::make_shared<ProcessTreeTestPeer>(std::move(annotators));
|
||||
self.initProc = self.tree->InsertInit();
|
||||
}
|
||||
|
||||
- (void)testSimpleOps {
|
||||
uint64_t event_id = 1;
|
||||
// PID 1.1: fork() -> PID 2.2
|
||||
const struct Pid child_pid = {.pid = 2, .pidversion = 2};
|
||||
self.tree->HandleFork(event_id++, *self.initProc, child_pid);
|
||||
|
||||
auto child_opt = self.tree->Get(child_pid);
|
||||
XCTAssertTrue(child_opt.has_value());
|
||||
std::shared_ptr<const Process> child = *child_opt;
|
||||
XCTAssertEqual(child->pid_, child_pid);
|
||||
XCTAssertEqual(child->program_, self.initProc->program_);
|
||||
XCTAssertEqual(child->effective_cred_, self.initProc->effective_cred_);
|
||||
XCTAssertEqual(self.tree->GetParent(*child), self.initProc);
|
||||
|
||||
// PID 2.2: exec("/bin/bash") -> PID 2.3
|
||||
const struct Pid child_exec_pid = {.pid = 2, .pidversion = 3};
|
||||
const struct Program child_exec_prog = {.executable = "/bin/bash",
|
||||
.arguments = {"/bin/bash", "-i"}};
|
||||
self.tree->HandleExec(event_id++, *child, child_exec_pid, child_exec_prog,
|
||||
child->effective_cred_);
|
||||
|
||||
child_opt = self.tree->Get(child_exec_pid);
|
||||
XCTAssertTrue(child_opt.has_value());
|
||||
child = *child_opt;
|
||||
XCTAssertEqual(child->pid_, child_exec_pid);
|
||||
XCTAssertEqual(*child->program_, child_exec_prog);
|
||||
XCTAssertEqual(child->effective_cred_, self.initProc->effective_cred_);
|
||||
}
|
||||
|
||||
// We can't test the full backfill process, as retrieving information on
|
||||
// processes (with task_name_for_pid) requires privileges.
|
||||
// Test what we can by LoadPID'ing ourselves.
|
||||
- (void)testLoadPID {
|
||||
auto proc = LoadPID(getpid()).value();
|
||||
|
||||
audit_token_t self_tok;
|
||||
mach_msg_type_number_t count = TASK_AUDIT_TOKEN_COUNT;
|
||||
XCTAssertEqual(task_info(mach_task_self(), TASK_AUDIT_TOKEN, (task_info_t)&self_tok, &count),
|
||||
KERN_SUCCESS);
|
||||
|
||||
XCTAssertEqual(proc.pid_.pid, audit_token_to_pid(self_tok));
|
||||
XCTAssertEqual(proc.pid_.pidversion, audit_token_to_pidversion(self_tok));
|
||||
|
||||
XCTAssertEqual(proc.effective_cred_.uid, geteuid());
|
||||
XCTAssertEqual(proc.effective_cred_.gid, getegid());
|
||||
|
||||
[[[NSProcessInfo processInfo] arguments]
|
||||
enumerateObjectsUsingBlock:^(NSString *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
|
||||
XCTAssertEqualObjects(@(proc.program_->arguments[idx].c_str()), obj);
|
||||
if (idx == 0) {
|
||||
XCTAssertEqualObjects(@(proc.program_->executable.c_str()), obj);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testAnnotation {
|
||||
std::vector<std::unique_ptr<Annotator>> annotators{};
|
||||
annotators.emplace_back(std::make_unique<TestAnnotator>());
|
||||
self.tree = std::make_shared<ProcessTreeTestPeer>(std::move(annotators));
|
||||
self.initProc = self.tree->InsertInit();
|
||||
|
||||
uint64_t event_id = 1;
|
||||
const struct Cred cred = {.uid = 0, .gid = 0};
|
||||
|
||||
// PID 1.1: fork() -> PID 2.2
|
||||
const struct Pid login_pid = {.pid = 2, .pidversion = 2};
|
||||
self.tree->HandleFork(event_id++, *self.initProc, login_pid);
|
||||
|
||||
// PID 2.2: exec("/usr/bin/login") -> PID 2.3
|
||||
const struct Pid login_exec_pid = {.pid = 2, .pidversion = 3};
|
||||
const struct Program login_prog = {.executable = std::string(kAnnotatedExecutable),
|
||||
.arguments = {}};
|
||||
auto login = *self.tree->Get(login_pid);
|
||||
self.tree->HandleExec(event_id++, *login, login_exec_pid, login_prog, cred);
|
||||
|
||||
// Ensure we have an annotation on login itself...
|
||||
login = *self.tree->Get(login_exec_pid);
|
||||
auto annotation = self.tree->GetAnnotation<TestAnnotator>(*login);
|
||||
XCTAssertTrue(annotation.has_value());
|
||||
|
||||
// PID 2.3: fork() -> PID 3.3
|
||||
const struct Pid shell_pid = {.pid = 3, .pidversion = 3};
|
||||
self.tree->HandleFork(event_id++, *login, shell_pid);
|
||||
// PID 3.3: exec("/bin/zsh") -> PID 3.4
|
||||
const struct Pid shell_exec_pid = {.pid = 3, .pidversion = 4};
|
||||
const struct Program shell_prog = {.executable = "/bin/zsh", .arguments = {}};
|
||||
auto shell = *self.tree->Get(shell_pid);
|
||||
self.tree->HandleExec(event_id++, *shell, shell_exec_pid, shell_prog, cred);
|
||||
|
||||
// ... and also ensure we have an annotation on the descendant zsh.
|
||||
shell = *self.tree->Get(shell_exec_pid);
|
||||
annotation = self.tree->GetAnnotation<TestAnnotator>(*shell);
|
||||
XCTAssertTrue(annotation.has_value());
|
||||
}
|
||||
|
||||
- (void)testCleanup {
|
||||
uint64_t event_id = 1;
|
||||
const struct Pid child_pid = {.pid = 2, .pidversion = 2};
|
||||
{
|
||||
self.tree->HandleFork(event_id++, *self.initProc, child_pid);
|
||||
auto child = *self.tree->Get(child_pid);
|
||||
self.tree->HandleExit(event_id++, *child);
|
||||
}
|
||||
|
||||
// We should still be able to get a handle to child...
|
||||
{
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertTrue(child.has_value());
|
||||
}
|
||||
|
||||
// ... until we step far enough into the future (32 events).
|
||||
struct Pid churn_pid = {.pid = 3, .pidversion = 3};
|
||||
for (int i = 0; i < 32; i++) {
|
||||
self.tree->HandleFork(event_id++, *self.initProc, churn_pid);
|
||||
churn_pid.pid++;
|
||||
}
|
||||
|
||||
// Now when we try processing the next event, it should have fallen out of the tree.
|
||||
self.tree->HandleFork(event_id++, *self.initProc, churn_pid);
|
||||
{
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertFalse(child.has_value());
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testRefcountCleanup {
|
||||
uint64_t event_id = 1;
|
||||
const struct Pid child_pid = {.pid = 2, .pidversion = 2};
|
||||
{
|
||||
self.tree->HandleFork(event_id++, *self.initProc, child_pid);
|
||||
auto child = *self.tree->Get(child_pid);
|
||||
self.tree->HandleExit(event_id++, *child);
|
||||
}
|
||||
|
||||
{
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertTrue(child.has_value());
|
||||
std::vector<struct Pid> pids = {(*child)->pid_};
|
||||
self.tree->RetainProcess(pids);
|
||||
}
|
||||
|
||||
// Even if we step far into the future, we should still be able to lookup
|
||||
// the child.
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
struct Pid churn_pid = {.pid = 100 + i, .pidversion = (uint64_t)(100 + i)};
|
||||
self.tree->HandleFork(event_id++, *self.initProc, churn_pid);
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertTrue(child.has_value());
|
||||
}
|
||||
|
||||
// But when released...
|
||||
{
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertTrue(child.has_value());
|
||||
std::vector<struct Pid> pids = {(*child)->pid_};
|
||||
self.tree->ReleaseProcess(pids);
|
||||
}
|
||||
|
||||
// ... it should immediately be removed.
|
||||
{
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertFalse(child.has_value());
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
32
Source/santad/ProcessTree/process_tree_test_helpers.h
Normal file
32
Source/santad/ProcessTree/process_tree_test_helpers.h
Normal file
@@ -0,0 +1,32 @@
|
||||
/// 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__SANTAD_PROCESSTREE_TREE_TEST_HELPERS_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_TREE_TEST_HELPERS_H
|
||||
#include <memory>
|
||||
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
class ProcessTreeTestPeer : public ProcessTree {
|
||||
public:
|
||||
explicit ProcessTreeTestPeer(
|
||||
std::vector<std::unique_ptr<Annotator>> &&annotators)
|
||||
: ProcessTree(std::move(annotators)) {}
|
||||
std::shared_ptr<const Process> InsertInit();
|
||||
};
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
42
Source/santad/ProcessTree/process_tree_test_helpers.mm
Normal file
42
Source/santad/ProcessTree/process_tree_test_helpers.mm
Normal file
@@ -0,0 +1,42 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
class ProcessTreeTestPeer : public ProcessTree {
|
||||
public:
|
||||
std::shared_ptr<const Process> InsertInit();
|
||||
};
|
||||
|
||||
std::shared_ptr<const Process> ProcessTreeTestPeer::InsertInit() {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
struct Pid initpid = {
|
||||
.pid = 1,
|
||||
.pidversion = 1,
|
||||
};
|
||||
auto proc = std::make_shared<Process>(
|
||||
initpid, (Cred){.uid = 0, .gid = 0},
|
||||
std::make_shared<Program>((Program){.executable = "/init", .arguments = {"/init"}}), nullptr);
|
||||
map_.emplace(initpid, proc);
|
||||
return proc;
|
||||
}
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
@@ -65,19 +65,21 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
// Adds a fake cached decision to SNTDecisionCache for pending files. If the file
|
||||
// is executed before we can create a transitive rule for it, then we can at
|
||||
// least log the pending decision info.
|
||||
- (void)saveFakeDecision:(const es_file_t *)esFile {
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] initWithEndpointSecurityFile:esFile];
|
||||
- (void)saveFakeDecision:(SNTFileInfo *)fileInfo {
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] initWithVnode:fileInfo.vnode];
|
||||
cd.decision = SNTEventStateAllowPendingTransitive;
|
||||
cd.sha256 = @"pending";
|
||||
[[SNTDecisionCache sharedCache] cacheDecision:cd];
|
||||
}
|
||||
|
||||
- (void)removeFakeDecision:(const es_file_t *)esFile {
|
||||
[[SNTDecisionCache sharedCache] forgetCachedDecisionForFile:esFile->stat];
|
||||
- (void)removeFakeDecision:(SNTFileInfo *)fileInfo {
|
||||
[[SNTDecisionCache sharedCache] forgetCachedDecisionForVnode:fileInfo.vnode];
|
||||
}
|
||||
|
||||
- (BOOL)handleEvent:(const Message &)esMsg withLogger:(std::shared_ptr<Logger>)logger {
|
||||
const es_file_t *targetFile = NULL;
|
||||
SNTFileInfo *targetFile;
|
||||
NSString *targetPath;
|
||||
NSError *error;
|
||||
|
||||
switch (esMsg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE:
|
||||
@@ -90,7 +92,9 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
return NO;
|
||||
}
|
||||
|
||||
targetFile = esMsg->event.close.target;
|
||||
targetPath = @(esMsg->event.close.target->path.data);
|
||||
targetFile = [[SNTFileInfo alloc] initWithEndpointSecurityFile:esMsg->event.close.target
|
||||
error:&error];
|
||||
|
||||
break;
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME:
|
||||
@@ -105,7 +109,24 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
return NO;
|
||||
}
|
||||
|
||||
targetFile = esMsg->event.rename.source;
|
||||
targetFile = [[SNTFileInfo alloc] initWithEndpointSecurityFile:esMsg->event.rename.source
|
||||
error:&error];
|
||||
if (!targetFile) {
|
||||
LOGD(@"Unable to locate source file for rename event while creating transitive. Falling "
|
||||
@"back to destination. Path: %s, Error: %@",
|
||||
esMsg->event.rename.source->path.data, error);
|
||||
if (esMsg->event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) {
|
||||
targetPath = @(esMsg->event.rename.destination.existing_file->path.data);
|
||||
targetFile = [[SNTFileInfo alloc]
|
||||
initWithEndpointSecurityFile:esMsg->event.rename.destination.existing_file
|
||||
error:&error];
|
||||
} else {
|
||||
targetPath = [NSString
|
||||
stringWithFormat:@"%s/%s", esMsg->event.rename.destination.new_path.dir->path.data,
|
||||
esMsg->event.rename.destination.new_path.filename.data];
|
||||
targetFile = [[SNTFileInfo alloc] initWithPath:targetPath error:&error];
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT:
|
||||
@@ -119,6 +140,9 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
[self createTransitiveRule:esMsg target:targetFile logger:logger];
|
||||
return YES;
|
||||
} else {
|
||||
LOGD(@"Unable to create SNTFileInfo while attempting to create transitive rule. Event: %d | "
|
||||
@"Path: %@ | Error: %@",
|
||||
(int)esMsg->event_type, targetPath, error);
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
@@ -127,31 +151,21 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
// compiler. It checks if the closed file is executable, and if so, transitively allowlists it.
|
||||
// The passed in message contains the pid of the writing process and path of closed file.
|
||||
- (void)createTransitiveRule:(const Message &)esMsg
|
||||
target:(const es_file_t *)targetFile
|
||||
target:(SNTFileInfo *)targetFile
|
||||
logger:(std::shared_ptr<Logger>)logger {
|
||||
NSError *error = nil;
|
||||
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithEndpointSecurityFile:targetFile error:&error];
|
||||
if (error) {
|
||||
LOGD(@"Unable to create SNTFileInfo while attempting to create transitive rule. Event: %d | "
|
||||
@"Path: %@ | Error: %@",
|
||||
(int)esMsg->event_type, @(targetFile->path.data), error);
|
||||
return;
|
||||
}
|
||||
|
||||
[self saveFakeDecision:targetFile];
|
||||
|
||||
// Check if this file is an executable.
|
||||
if (fi.isExecutable) {
|
||||
if (targetFile.isExecutable) {
|
||||
// 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
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil];
|
||||
SNTRule *prevRule = [ruleTable ruleForIdentifiers:(struct RuleIdentifiers){
|
||||
.binarySHA256 = targetFile.SHA256,
|
||||
}];
|
||||
if (!prevRule || prevRule.state == SNTRuleStateAllowTransitive) {
|
||||
// Construct a new transitive allowlist rule for the executable.
|
||||
SNTRule *rule = [[SNTRule alloc] initWithIdentifier:fi.SHA256
|
||||
SNTRule *rule = [[SNTRule alloc] initWithIdentifier:targetFile.SHA256
|
||||
state:SNTRuleStateAllowTransitive
|
||||
type:SNTRuleTypeBinary
|
||||
customMsg:@""];
|
||||
@@ -161,7 +175,7 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
if (![ruleTable addRules:@[ rule ] ruleCleanup:SNTRuleCleanupNone error:&err]) {
|
||||
LOGE(@"unable to add new transitive rule to database: %@", err.localizedDescription);
|
||||
} else {
|
||||
logger->LogAllowlist(esMsg, [fi.SHA256 UTF8String]);
|
||||
logger->LogAllowlist(esMsg, [targetFile.SHA256 UTF8String]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,16 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/SNTCompilerController.h"
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include "Source/santad/SNTCompilerController.h"
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <string_view>
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
@@ -39,10 +39,10 @@ static const pid_t PID_MAX = 99999;
|
||||
|
||||
@interface SNTCompilerController (Testing)
|
||||
- (BOOL)isCompiler:(const audit_token_t &)tok;
|
||||
- (void)saveFakeDecision:(const es_file_t *)esFile;
|
||||
- (void)removeFakeDecision:(const es_file_t *)esFile;
|
||||
- (void)saveFakeDecision:(SNTFileInfo *)esFile;
|
||||
- (void)removeFakeDecision:(SNTFileInfo *)esFile;
|
||||
- (void)createTransitiveRule:(const Message &)esMsg
|
||||
target:(const es_file_t *)targetFile
|
||||
target:(SNTFileInfo *)targetFile
|
||||
logger:(std::shared_ptr<Logger>)logger;
|
||||
@end
|
||||
|
||||
@@ -117,34 +117,39 @@ static const pid_t PID_MAX = 99999;
|
||||
}
|
||||
|
||||
- (void)testSaveFakeDecision {
|
||||
es_file_t file = MakeESFile("foo", {
|
||||
.st_dev = 12,
|
||||
.st_ino = 34,
|
||||
});
|
||||
SantaVnode vnode{
|
||||
.fsid = 12,
|
||||
.fileid = 34,
|
||||
};
|
||||
|
||||
OCMExpect([self.mockDecisionCache
|
||||
cacheDecision:[OCMArg checkWithBlock:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.vnodeId.fsid == file.stat.st_dev && cd.vnodeId.fileid == file.stat.st_ino &&
|
||||
cd.decision == SNTEventStateAllowPendingTransitive &&
|
||||
return cd.vnodeId == vnode && cd.decision == SNTEventStateAllowPendingTransitive &&
|
||||
[cd.sha256 isEqualToString:@"pending"];
|
||||
}]]);
|
||||
|
||||
id mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
OCMExpect([mockFileInfo vnode]).andReturn(vnode);
|
||||
|
||||
SNTCompilerController *cc = [[SNTCompilerController alloc] init];
|
||||
[cc saveFakeDecision:&file];
|
||||
[cc saveFakeDecision:mockFileInfo];
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(self.mockDecisionCache), "Unable to verify all expectations");
|
||||
}
|
||||
|
||||
- (void)testRemoveFakeDecision {
|
||||
es_file_t file = MakeESFile("foo", {
|
||||
.st_dev = 12,
|
||||
.st_ino = 34,
|
||||
});
|
||||
SantaVnode vnode{
|
||||
.fsid = 12,
|
||||
.fileid = 34,
|
||||
};
|
||||
|
||||
OCMExpect([self.mockDecisionCache forgetCachedDecisionForFile:file.stat]);
|
||||
id mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
OCMExpect([mockFileInfo vnode]).andReturn(vnode);
|
||||
|
||||
OCMExpect([self.mockDecisionCache forgetCachedDecisionForVnode:vnode]);
|
||||
|
||||
SNTCompilerController *cc = [[SNTCompilerController alloc] init];
|
||||
[cc removeFakeDecision:&file];
|
||||
[cc removeFakeDecision:mockFileInfo];
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(self.mockDecisionCache), "Unable to verify all expectations");
|
||||
}
|
||||
@@ -153,6 +158,7 @@ static const pid_t PID_MAX = 99999;
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_file_t ignoredFile = MakeESFile("/dev/bar");
|
||||
es_file_t normalFile = MakeESFile("bar");
|
||||
SantaVnode vnodeNormal = SantaVnode::VnodeForFile(&normalFile);
|
||||
audit_token_t compilerTok = MakeAuditToken(12, 34);
|
||||
audit_token_t notCompilerTok = MakeAuditToken(56, 78);
|
||||
es_process_t compilerProc = MakeESProcess(&file, compilerTok, {});
|
||||
@@ -221,33 +227,140 @@ static const pid_t PID_MAX = 99999;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
id mockCompilerController = OCMPartialMock(cc);
|
||||
id mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
OCMStub([mockFileInfo alloc]).andReturn(mockFileInfo);
|
||||
OCMStub([mockFileInfo initWithEndpointSecurityFile:&normalFile error:[OCMArg anyObjectRef]])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(mockFileInfo);
|
||||
OCMStub([mockFileInfo vnode]).andReturn(vnodeNormal);
|
||||
|
||||
OCMExpect([mockCompilerController createTransitiveRule:msg
|
||||
target:esMsg.event.close.target
|
||||
logger:nullptr])
|
||||
OCMExpect([mockCompilerController
|
||||
createTransitiveRule:msg
|
||||
target:[OCMArg checkWithBlock:^BOOL(SNTFileInfo *fi) {
|
||||
return fi.vnode.fsid == normalFile.stat.st_dev &&
|
||||
fi.vnode.fileid == normalFile.stat.st_ino;
|
||||
}]
|
||||
logger:nullptr])
|
||||
.ignoringNonObjectArgs();
|
||||
|
||||
XCTAssertTrue([cc handleEvent:msg withLogger:nullptr]);
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockCompilerController), "Unable to verify all expectations");
|
||||
[mockCompilerController stopMocking];
|
||||
[mockFileInfo stopMocking];
|
||||
}
|
||||
// Ensure transitive rules are created for RENAME events from the source path
|
||||
{
|
||||
esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_RENAME, &compilerProc);
|
||||
esMsg.event.rename.source = &normalFile;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
id mockCompilerController = OCMPartialMock(cc);
|
||||
id mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
OCMStub([mockFileInfo alloc]).andReturn(mockFileInfo);
|
||||
OCMStub([mockFileInfo initWithEndpointSecurityFile:&normalFile error:[OCMArg anyObjectRef]])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(mockFileInfo);
|
||||
OCMStub([mockFileInfo vnode]).andReturn(vnodeNormal);
|
||||
|
||||
OCMExpect([mockCompilerController createTransitiveRule:msg
|
||||
target:esMsg.event.close.target
|
||||
logger:nullptr])
|
||||
OCMExpect([mockCompilerController
|
||||
createTransitiveRule:msg
|
||||
target:[OCMArg checkWithBlock:^BOOL(SNTFileInfo *fi) {
|
||||
return fi.vnode.fsid == normalFile.stat.st_dev &&
|
||||
fi.vnode.fileid == normalFile.stat.st_ino;
|
||||
}]
|
||||
logger:nullptr])
|
||||
.ignoringNonObjectArgs();
|
||||
|
||||
XCTAssertTrue([cc handleEvent:msg withLogger:nullptr]);
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockCompilerController), "Unable to verify all expectations");
|
||||
[mockCompilerController stopMocking];
|
||||
[mockFileInfo stopMocking];
|
||||
}
|
||||
// Ensure transitive rules are created for RENAME events from the existing destinatio path as a
|
||||
// fallback
|
||||
{
|
||||
es_file_t destFile = MakeESFile("dest", MakeStat(1000));
|
||||
esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_RENAME, &compilerProc);
|
||||
esMsg.event.rename.source = &normalFile;
|
||||
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
|
||||
esMsg.event.rename.destination.existing_file = &destFile;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
SantaVnode vnodeDest = SantaVnode::VnodeForFile(&destFile);
|
||||
|
||||
id mockCompilerController = OCMPartialMock(cc);
|
||||
id mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
OCMStub([mockFileInfo alloc]).andReturn(mockFileInfo);
|
||||
// Return nil the first time when the source path is looked up
|
||||
OCMExpect([mockFileInfo initWithEndpointSecurityFile:&normalFile error:[OCMArg anyObjectRef]])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(nil);
|
||||
OCMExpect([mockFileInfo initWithEndpointSecurityFile:&destFile error:[OCMArg anyObjectRef]])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(mockFileInfo);
|
||||
OCMStub([mockFileInfo vnode]).andReturn(vnodeDest);
|
||||
|
||||
OCMExpect([mockCompilerController
|
||||
createTransitiveRule:msg
|
||||
target:[OCMArg checkWithBlock:^BOOL(SNTFileInfo *fi) {
|
||||
return fi.vnode.fsid == destFile.stat.st_dev &&
|
||||
fi.vnode.fileid == destFile.stat.st_ino;
|
||||
}]
|
||||
logger:nullptr])
|
||||
.ignoringNonObjectArgs();
|
||||
|
||||
XCTAssertTrue([cc handleEvent:msg withLogger:nullptr]);
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockCompilerController), "Unable to verify all expectations");
|
||||
[mockCompilerController stopMocking];
|
||||
[mockFileInfo stopMocking];
|
||||
}
|
||||
// Ensure transitive rules are created for RENAME events from the existing destinatio path as a
|
||||
// fallback
|
||||
{
|
||||
es_file_t destDir = MakeESFile("/usr/bin", MakeStat(1000));
|
||||
es_string_token_t destFilename = MakeESStringToken("true");
|
||||
esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_RENAME, &compilerProc);
|
||||
esMsg.event.rename.source = &normalFile;
|
||||
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
|
||||
esMsg.event.rename.destination.new_path.dir = &destDir;
|
||||
esMsg.event.rename.destination.new_path.filename = destFilename;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
NSString *expectedTarget =
|
||||
[NSString stringWithFormat:@"%s/%s", destDir.path.data, destFilename.data];
|
||||
|
||||
struct stat sbNewFile;
|
||||
XCTAssertEqual(stat("/usr/bin/true", &sbNewFile), 0);
|
||||
SantaVnode vnodeDest = SantaVnode::VnodeForFile(sbNewFile);
|
||||
|
||||
id mockCompilerController = OCMPartialMock(cc);
|
||||
id mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
OCMStub([mockFileInfo alloc]).andReturn(mockFileInfo);
|
||||
OCMStub([mockFileInfo vnode]).andReturn(vnodeDest);
|
||||
|
||||
// Return nil the first time when the source path is looked up
|
||||
OCMExpect([mockFileInfo initWithEndpointSecurityFile:&normalFile error:[OCMArg anyObjectRef]])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(nil);
|
||||
OCMExpect([mockFileInfo initWithPath:expectedTarget error:[OCMArg anyObjectRef]])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(mockFileInfo);
|
||||
|
||||
OCMExpect([mockCompilerController
|
||||
createTransitiveRule:msg
|
||||
target:[OCMArg checkWithBlock:^BOOL(SNTFileInfo *fi) {
|
||||
return fi.vnode.fsid == sbNewFile.st_dev &&
|
||||
fi.vnode.fileid == sbNewFile.st_ino;
|
||||
}]
|
||||
logger:nullptr])
|
||||
.ignoringNonObjectArgs();
|
||||
|
||||
XCTAssertTrue([cc handleEvent:msg withLogger:nullptr]);
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockCompilerController), "Unable to verify all expectations");
|
||||
[mockCompilerController stopMocking];
|
||||
[mockFileInfo stopMocking];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/common/SNTStrengthify.h"
|
||||
#import "Source/common/SNTXPCNotifierInterface.h"
|
||||
@@ -97,11 +98,19 @@ 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, int64_t signingID))reply {
|
||||
- (void)databaseRuleCounts:(void (^)(RuleCounts ruleTypeCounts))reply {
|
||||
SNTRuleTable *rdb = [SNTDatabaseController ruleTable];
|
||||
reply([rdb binaryRuleCount], [rdb certificateRuleCount], [rdb compilerRuleCount],
|
||||
[rdb transitiveRuleCount], [rdb teamIDRuleCount], [rdb signingIDRuleCount]);
|
||||
RuleCounts ruleCounts{
|
||||
.binary = [rdb binaryRuleCount],
|
||||
.certificate = [rdb certificateRuleCount],
|
||||
.compiler = [rdb compilerRuleCount],
|
||||
.transitive = [rdb transitiveRuleCount],
|
||||
.teamID = [rdb teamIDRuleCount],
|
||||
.signingID = [rdb signingIDRuleCount],
|
||||
.cdhash = [rdb cdhashRuleCount],
|
||||
};
|
||||
|
||||
reply(ruleCounts);
|
||||
}
|
||||
|
||||
- (void)databaseRuleAddRules:(NSArray *)rules
|
||||
@@ -142,15 +151,9 @@ double watchdogRAMPeak = 0;
|
||||
[[SNTDatabaseController eventTable] deleteEventsWithIds:ids];
|
||||
}
|
||||
|
||||
- (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]);
|
||||
- (void)databaseRuleForIdentifiers:(SNTRuleIdentifiers *)identifiers
|
||||
reply:(void (^)(SNTRule *))reply {
|
||||
reply([[SNTDatabaseController ruleTable] ruleForIdentifiers:[identifiers toStruct]]);
|
||||
}
|
||||
|
||||
- (void)staticRuleCount:(void (^)(int64_t count))reply {
|
||||
@@ -175,17 +178,9 @@ double watchdogRAMPeak = 0;
|
||||
#pragma mark Decision Ops
|
||||
|
||||
- (void)decisionForFilePath:(NSString *)filePath
|
||||
fileSHA256:(NSString *)fileSHA256
|
||||
certificateSHA256:(NSString *)certificateSHA256
|
||||
teamID:(NSString *)teamID
|
||||
signingID:(NSString *)signingID
|
||||
identifiers:(SNTRuleIdentifiers *)identifiers
|
||||
reply:(void (^)(SNTEventState))reply {
|
||||
reply([self.policyProcessor decisionForFilePath:filePath
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
signingID:signingID]
|
||||
.decision);
|
||||
reply([self.policyProcessor decisionForFilePath:filePath identifiers:identifiers].decision);
|
||||
}
|
||||
|
||||
#pragma mark Config Ops
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
|
||||
@interface SNTDecisionCache : NSObject
|
||||
|
||||
@@ -24,7 +25,7 @@
|
||||
|
||||
- (void)cacheDecision:(SNTCachedDecision *)cd;
|
||||
- (SNTCachedDecision *)cachedDecisionForFile:(const struct stat &)statInfo;
|
||||
- (void)forgetCachedDecisionForFile:(const struct stat &)statInfo;
|
||||
- (void)forgetCachedDecisionForVnode:(SantaVnode)vnode;
|
||||
- (SNTCachedDecision *)resetTimestampForCachedDecision:(const struct stat &)statInfo;
|
||||
|
||||
@end
|
||||
|
||||
@@ -58,8 +58,8 @@
|
||||
return self->_decisionCache.get(SantaVnode::VnodeForFile(statInfo));
|
||||
}
|
||||
|
||||
- (void)forgetCachedDecisionForFile:(const struct stat &)statInfo {
|
||||
self->_decisionCache.remove(SantaVnode::VnodeForFile(statInfo));
|
||||
- (void)forgetCachedDecisionForVnode:(SantaVnode)vnode {
|
||||
self->_decisionCache.remove(vnode);
|
||||
}
|
||||
|
||||
// Whenever a cached decision resulting from a transitive allowlist rule is used to allow the
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
#import "Source/santad/SNTDatabaseController.h"
|
||||
@@ -70,7 +71,7 @@ SNTCachedDecision *MakeCachedDecision(struct stat sb, SNTEventState decision) {
|
||||
XCTAssertEqual(cachedCD.vnodeId.fileid, cd.vnodeId.fileid);
|
||||
|
||||
// Delete the item from the cache and ensure it no longer exists
|
||||
[dc forgetCachedDecisionForFile:sb];
|
||||
[dc forgetCachedDecisionForVnode:SantaVnode::VnodeForFile(sb)];
|
||||
XCTAssertNil([dc cachedDecisionForFile:sb]);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ const static NSString *kBlockTeamID = @"BlockTeamID";
|
||||
const static NSString *kAllowTeamID = @"AllowTeamID";
|
||||
const static NSString *kBlockSigningID = @"BlockSigningID";
|
||||
const static NSString *kAllowSigningID = @"AllowSigningID";
|
||||
const static NSString *kBlockCDHash = @"BlockCDHash";
|
||||
const static NSString *kAllowCDHash = @"AllowCDHash";
|
||||
const static NSString *kBlockScope = @"BlockScope";
|
||||
const static NSString *kAllowScope = @"AllowScope";
|
||||
const static NSString *kAllowUnknown = @"AllowUnknown";
|
||||
|
||||
@@ -162,6 +162,8 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
case SNTEventStateAllowTeamID: eventTypeStr = kAllowTeamID; break;
|
||||
case SNTEventStateBlockSigningID: eventTypeStr = kBlockSigningID; break;
|
||||
case SNTEventStateAllowSigningID: eventTypeStr = kAllowSigningID; break;
|
||||
case SNTEventStateBlockCDHash: eventTypeStr = kBlockCDHash; break;
|
||||
case SNTEventStateAllowCDHash: eventTypeStr = kAllowCDHash; break;
|
||||
case SNTEventStateBlockScope: eventTypeStr = kBlockScope; break;
|
||||
case SNTEventStateAllowScope: eventTypeStr = kAllowScope; break;
|
||||
case SNTEventStateBlockUnknown: eventTypeStr = kBlockUnknown; break;
|
||||
@@ -230,7 +232,7 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
SNTFileInfo *binInfo = [[SNTFileInfo alloc] initWithEndpointSecurityFile:targetProc->executable
|
||||
error:&fileInfoError];
|
||||
if (unlikely(!binInfo)) {
|
||||
if (config.failClosed && config.clientMode == SNTClientModeLockdown) {
|
||||
if (config.failClosed) {
|
||||
LOGE(@"Failed to read file %@: %@ and denying action", @(targetProc->executable->path.data),
|
||||
fileInfoError.localizedDescription);
|
||||
postAction(SNTActionRespondDeny);
|
||||
@@ -326,6 +328,7 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
se.signingChain = cd.certChain;
|
||||
se.teamID = cd.teamID;
|
||||
se.signingID = cd.signingID;
|
||||
se.cdhash = cd.cdhash;
|
||||
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());
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTMetricSet.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#import "Source/santad/DataLayer/SNTEventTable.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
@@ -201,6 +202,7 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
});
|
||||
es_process_t procExec = MakeESProcess(&fileExec);
|
||||
procExec.is_platform_binary = false;
|
||||
procExec.codesigning_flags = CS_SIGNED | CS_VALID;
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc);
|
||||
esMsg.event.exec.target = &procExec;
|
||||
|
||||
@@ -223,6 +225,22 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
[self validateExecEvent:wantAction messageSetup:nil];
|
||||
}
|
||||
|
||||
- (void)stubRule:(SNTRule *)rule forIdentifiers:(struct RuleIdentifiers)wantIdentifiers {
|
||||
OCMStub([self.mockRuleDatabase ruleForIdentifiers:wantIdentifiers])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(^(NSInvocation *inv) {
|
||||
struct RuleIdentifiers gotIdentifiers = {};
|
||||
[inv getArgument:&gotIdentifiers atIndex:2];
|
||||
|
||||
XCTAssertEqualObjects(gotIdentifiers.cdhash, wantIdentifiers.cdhash);
|
||||
XCTAssertEqualObjects(gotIdentifiers.binarySHA256, wantIdentifiers.binarySHA256);
|
||||
XCTAssertEqualObjects(gotIdentifiers.signingID, wantIdentifiers.signingID);
|
||||
XCTAssertEqualObjects(gotIdentifiers.certificateSHA256, wantIdentifiers.certificateSHA256);
|
||||
XCTAssertEqualObjects(gotIdentifiers.teamID, wantIdentifiers.teamID);
|
||||
})
|
||||
.andReturn(rule);
|
||||
}
|
||||
|
||||
- (void)testBinaryAllowRule {
|
||||
OCMStub([self.mockFileInfo isMachO]).andReturn(YES);
|
||||
OCMStub([self.mockFileInfo SHA256]).andReturn(@"a");
|
||||
@@ -230,11 +248,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.binarySHA256 = @"a"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowBinary expected:@1];
|
||||
@@ -247,16 +262,96 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateBlock;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.binarySHA256 = @"a"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny];
|
||||
[self checkMetricCounters:kBlockBinary expected:@1];
|
||||
}
|
||||
|
||||
- (void)testCDHashAllowRule {
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeCDHash;
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.cdhash = @"aa00000000000000000000000000000000000000"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->cdhash[0] = 0xaa;
|
||||
msg->event.exec.target->codesigning_flags = CS_SIGNED | CS_VALID | CS_KILL | CS_HARD;
|
||||
}];
|
||||
[self checkMetricCounters:kAllowCDHash expected:@1];
|
||||
}
|
||||
|
||||
- (void)testCDHashNoHardenedRuntimeRule {
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeCDHash;
|
||||
|
||||
// No CDHash should be set when hardened runtime CS flags are not set
|
||||
[self stubRule:rule forIdentifiers:{.cdhash = nil}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->cdhash[0] = 0xaa;
|
||||
// Ensure CS_HARD and CS_KILL are not set
|
||||
msg->event.exec.target->codesigning_flags = CS_SIGNED | CS_VALID;
|
||||
}];
|
||||
[self checkMetricCounters:kAllowCDHash expected:@1];
|
||||
}
|
||||
|
||||
- (void)testCDHashBlockRule {
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateBlock;
|
||||
rule.type = SNTRuleTypeCDHash;
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.cdhash = @"aa00000000000000000000000000000000000000"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->cdhash[0] = 0xaa;
|
||||
msg->event.exec.target->codesigning_flags = CS_SIGNED | CS_VALID | CS_KILL | CS_HARD;
|
||||
}];
|
||||
[self checkMetricCounters:kBlockCDHash expected:@1];
|
||||
}
|
||||
|
||||
- (void)testCDHashAllowCompilerRule {
|
||||
OCMStub([self.mockConfigurator enableTransitiveRules]).andReturn(YES);
|
||||
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowCompiler;
|
||||
rule.type = SNTRuleTypeCDHash;
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.cdhash = @"aa00000000000000000000000000000000000000"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllowCompiler
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->cdhash[0] = 0xaa;
|
||||
msg->event.exec.target->codesigning_flags = CS_SIGNED | CS_VALID | CS_KILL | CS_HARD;
|
||||
}];
|
||||
|
||||
[self checkMetricCounters:kAllowCompiler expected:@1];
|
||||
}
|
||||
|
||||
- (void)testCDHashAllowCompilerRuleTransitiveRuleDisabled {
|
||||
OCMStub([self.mockConfigurator enableTransitiveRules]).andReturn(NO);
|
||||
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowCompiler;
|
||||
rule.type = SNTRuleTypeCDHash;
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.cdhash = @"aa00000000000000000000000000000000000000"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->cdhash[0] = 0xaa;
|
||||
msg->event.exec.target->codesigning_flags = CS_SIGNED | CS_VALID | CS_KILL | CS_HARD;
|
||||
}];
|
||||
|
||||
[self checkMetricCounters:kAllowCDHash expected:@1];
|
||||
}
|
||||
|
||||
- (void)testSigningIDAllowRule {
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
@@ -264,17 +359,14 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
|
||||
NSString *signingID = [NSString stringWithFormat:@"%s:%s", kExampleTeamID, kExampleSigningID];
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:signingID
|
||||
certificateSHA256:nil
|
||||
teamID:@(kExampleTeamID)])
|
||||
.andReturn(rule);
|
||||
[self stubRule:rule forIdentifiers:{ .signingID = signingID, .teamID = @(kExampleTeamID) }];
|
||||
|
||||
[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];
|
||||
}
|
||||
|
||||
@@ -284,12 +376,7 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
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 stubRule:rule forIdentifiers:{ .signingID = signingID, .teamID = @(kExampleTeamID) }];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
@@ -308,11 +395,7 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeTeamID;
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@(kExampleTeamID)])
|
||||
.andReturn(rule);
|
||||
[self stubRule:rule forIdentifiers:{ .teamID = @(kExampleTeamID) }];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
@@ -330,11 +413,7 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
rule.state = SNTRuleStateBlock;
|
||||
rule.type = SNTRuleTypeTeamID;
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:@(kExampleTeamID)])
|
||||
.andReturn(rule);
|
||||
[self stubRule:rule forIdentifiers:{ .teamID = @(kExampleTeamID) }];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
@@ -353,11 +432,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeCertificate;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:@"a"
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.certificateSHA256 = @"a"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowCertificate expected:@1];
|
||||
@@ -373,11 +449,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateBlock;
|
||||
rule.type = SNTRuleTypeCertificate;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil
|
||||
signingID:nil
|
||||
certificateSHA256:@"a"
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.certificateSHA256 = @"a"}];
|
||||
|
||||
OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]);
|
||||
|
||||
@@ -395,11 +468,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowCompiler;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.binarySHA256 = @"a"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllowCompiler];
|
||||
[self checkMetricCounters:kAllowCompiler expected:@1];
|
||||
@@ -413,11 +483,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowCompiler;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.binarySHA256 = @"a"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowBinary expected:@1];
|
||||
@@ -431,11 +498,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowTransitive;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.binarySHA256 = @"a"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowTransitive expected:@1];
|
||||
@@ -450,11 +514,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllowTransitive;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.binarySHA256 = @"a"}];
|
||||
|
||||
OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]);
|
||||
|
||||
@@ -477,11 +538,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
rule.state = SNTRuleStateAllowCompiler;
|
||||
rule.type = SNTRuleTypeSigningID;
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:signingID
|
||||
certificateSHA256:nil
|
||||
teamID:@(kExampleTeamID)])
|
||||
.andReturn(rule);
|
||||
[self stubRule:rule
|
||||
forIdentifiers:{ .binarySHA256 = @"a", .signingID = signingID, .teamID = @(kExampleTeamID) }];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllowCompiler
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
@@ -502,20 +560,11 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
rule.state = SNTRuleStateAllowTransitive;
|
||||
rule.type = SNTRuleTypeSigningID;
|
||||
|
||||
NSString *signingID = [NSString stringWithFormat:@"%s:%s", kExampleTeamID, kExampleSigningID];
|
||||
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:signingID
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
[self stubRule:rule forIdentifiers:{.binarySHA256 = @"a"}];
|
||||
|
||||
OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
msg->event.exec.target->signing_id = MakeESStringToken("com.google.santa.test");
|
||||
}];
|
||||
[self validateExecEvent:SNTActionRespondDeny];
|
||||
|
||||
OCMVerifyAllWithDelay(self.mockEventDatabase, 1);
|
||||
[self checkMetricCounters:kAllowSigningID expected:@0];
|
||||
@@ -557,7 +606,7 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
[self checkMetricCounters:kAllowUnknown expected:@1];
|
||||
}
|
||||
|
||||
- (void)testUnreadableFailOpenLockdown {
|
||||
- (void)testUnreadableFailOpen {
|
||||
// Undo the default mocks
|
||||
[self.mockFileInfo stopMocking];
|
||||
self.mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
@@ -565,15 +614,13 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
OCMStub([self.mockFileInfo alloc]).andReturn(nil);
|
||||
OCMStub([self.mockFileInfo initWithPath:OCMOCK_ANY error:[OCMArg setTo:nil]]).andReturn(nil);
|
||||
|
||||
// Lockdown mode, no fail-closed
|
||||
OCMStub([self.mockConfigurator failClosed]).andReturn(NO);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowNoFileInfo expected:@1];
|
||||
}
|
||||
|
||||
- (void)testUnreadableFailClosedLockdown {
|
||||
- (void)testUnreadableFailClosed {
|
||||
// Undo the default mocks
|
||||
[self.mockFileInfo stopMocking];
|
||||
self.mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
@@ -581,30 +628,12 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
OCMStub([self.mockFileInfo alloc]).andReturn(nil);
|
||||
OCMStub([self.mockFileInfo initWithPath:OCMOCK_ANY error:[OCMArg setTo:nil]]).andReturn(nil);
|
||||
|
||||
// Lockdown mode, fail-closed
|
||||
OCMStub([self.mockConfigurator failClosed]).andReturn(YES);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny];
|
||||
[self checkMetricCounters:kDenyNoFileInfo expected:@1];
|
||||
}
|
||||
|
||||
- (void)testUnreadableFailClosedMonitor {
|
||||
// Undo the default mocks
|
||||
[self.mockFileInfo stopMocking];
|
||||
self.mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
|
||||
OCMStub([self.mockFileInfo alloc]).andReturn(nil);
|
||||
OCMStub([self.mockFileInfo initWithPath:OCMOCK_ANY error:[OCMArg setTo:nil]]).andReturn(nil);
|
||||
|
||||
// Monitor mode, fail-closed
|
||||
OCMStub([self.mockConfigurator failClosed]).andReturn(YES);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeMonitor);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowNoFileInfo expected:@1];
|
||||
}
|
||||
|
||||
- (void)testMissingShasum {
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowScope expected:@1];
|
||||
@@ -638,11 +667,8 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.state = SNTRuleStateAllow;
|
||||
rule.type = SNTRuleTypeBinary;
|
||||
OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a"
|
||||
signingID:nil
|
||||
certificateSHA256:nil
|
||||
teamID:nil])
|
||||
.andReturn(rule);
|
||||
|
||||
[self stubRule:rule forIdentifiers:{.binarySHA256 = @"a"}];
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
OCMVerifyAllWithDelay(self.mockEventDatabase, 1);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTRuleIdentifiers.h"
|
||||
|
||||
@class MOLCodesignChecker;
|
||||
@class SNTCachedDecision;
|
||||
@@ -57,9 +58,6 @@
|
||||
/// calculated, use the fileSHA256 parameter to save a second calculation of the hash.
|
||||
///
|
||||
- (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID;
|
||||
identifiers:(nonnull SNTRuleIdentifiers *)identifiers;
|
||||
|
||||
@end
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
}
|
||||
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
cdhash:(nullable NSString *)cdhash
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID
|
||||
@@ -54,6 +55,7 @@
|
||||
(NSDictionary *_Nullable (^_Nullable)(
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.cdhash = cdhash;
|
||||
cd.sha256 = fileSHA256 ?: fileInfo.SHA256;
|
||||
cd.teamID = teamID;
|
||||
cd.signingID = signingID;
|
||||
@@ -74,7 +76,8 @@
|
||||
cd.certSHA256 = certificateSHA256;
|
||||
} else {
|
||||
// Grab the code signature, if there's an error don't try to capture
|
||||
// any of the signature details.
|
||||
// any of the signature details. Also clear out any rule lookup parameters
|
||||
// that would require being validly signed.
|
||||
MOLCodesignChecker *csInfo = [fileInfo codesignCheckerWithError:&csInfoError];
|
||||
if (csInfoError) {
|
||||
csInfo = nil;
|
||||
@@ -82,6 +85,7 @@
|
||||
[NSString stringWithFormat:@"Signature ignored due to error: %ld", (long)csInfoError.code];
|
||||
cd.teamID = nil;
|
||||
cd.signingID = nil;
|
||||
cd.cdhash = nil;
|
||||
} else {
|
||||
cd.certSHA256 = csInfo.leafCertificate.SHA256;
|
||||
cd.certCommonName = csInfo.leafCertificate.commonName;
|
||||
@@ -127,12 +131,37 @@
|
||||
cd.signingID = nil;
|
||||
}
|
||||
|
||||
SNTRule *rule = [self.ruleTable ruleForBinarySHA256:cd.sha256
|
||||
signingID:cd.signingID
|
||||
certificateSHA256:cd.certSHA256
|
||||
teamID:cd.teamID];
|
||||
SNTRule *rule =
|
||||
[self.ruleTable ruleForIdentifiers:(struct RuleIdentifiers){.cdhash = cd.cdhash,
|
||||
.binarySHA256 = cd.sha256,
|
||||
.signingID = cd.signingID,
|
||||
.certificateSHA256 = cd.certSHA256,
|
||||
.teamID = cd.teamID}];
|
||||
if (rule) {
|
||||
switch (rule.type) {
|
||||
case SNTRuleTypeCDHash:
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowCDHash; return cd;
|
||||
case SNTRuleStateAllowCompiler:
|
||||
// If transitive rules are enabled, then SNTRuleStateAllowListCompiler rules
|
||||
// become SNTEventStateAllowCompiler decisions. Otherwise we treat the rule as if
|
||||
// it were SNTRuleStateAllowCDHash.
|
||||
if ([self.configurator enableTransitiveRules]) {
|
||||
cd.decision = SNTEventStateAllowCompiler;
|
||||
} else {
|
||||
cd.decision = SNTEventStateAllowCDHash;
|
||||
}
|
||||
return cd;
|
||||
case SNTRuleStateSilentBlock:
|
||||
cd.silentBlock = YES;
|
||||
// intentional fallthrough
|
||||
case SNTRuleStateBlock:
|
||||
cd.customMsg = rule.customMsg;
|
||||
cd.customURL = rule.customURL;
|
||||
cd.decision = SNTEventStateBlockCDHash;
|
||||
return cd;
|
||||
default: break;
|
||||
}
|
||||
case SNTRuleTypeBinary:
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllow: cd.decision = SNTEventStateAllowBinary; return cd;
|
||||
@@ -259,25 +288,43 @@
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
|
||||
NSString *signingID;
|
||||
NSString *teamID;
|
||||
NSString *cdhash;
|
||||
|
||||
const char *entitlementsFilterTeamID = NULL;
|
||||
|
||||
if (targetProc->signing_id.length > 0) {
|
||||
if (targetProc->team_id.length > 0) {
|
||||
entitlementsFilterTeamID = targetProc->team_id.data;
|
||||
teamID = [NSString stringWithUTF8String:targetProc->team_id.data];
|
||||
signingID =
|
||||
[NSString stringWithFormat:@"%@:%@", teamID,
|
||||
[NSString stringWithUTF8String:targetProc->signing_id.data]];
|
||||
} else if (targetProc->is_platform_binary) {
|
||||
entitlementsFilterTeamID = "platform";
|
||||
signingID =
|
||||
[NSString stringWithFormat:@"platform:%@",
|
||||
[NSString stringWithUTF8String:targetProc->signing_id.data]];
|
||||
if (targetProc->codesigning_flags & CS_SIGNED && targetProc->codesigning_flags & CS_VALID) {
|
||||
if (targetProc->signing_id.length > 0) {
|
||||
if (targetProc->team_id.length > 0) {
|
||||
entitlementsFilterTeamID = targetProc->team_id.data;
|
||||
teamID = [NSString stringWithUTF8String:targetProc->team_id.data];
|
||||
signingID =
|
||||
[NSString stringWithFormat:@"%@:%@", teamID,
|
||||
[NSString stringWithUTF8String:targetProc->signing_id.data]];
|
||||
} else if (targetProc->is_platform_binary) {
|
||||
entitlementsFilterTeamID = "platform";
|
||||
signingID =
|
||||
[NSString stringWithFormat:@"platform:%@",
|
||||
[NSString stringWithUTF8String:targetProc->signing_id.data]];
|
||||
}
|
||||
}
|
||||
|
||||
// Only consider the CDHash for processes that have CS_KILL or CS_HARD set.
|
||||
// This ensures that the OS will kill the process if the CDHash was tampered
|
||||
// with and code was loaded that didn't match a page hash.
|
||||
if (targetProc->codesigning_flags & CS_KILL || targetProc->codesigning_flags & CS_HARD) {
|
||||
static NSString *const kCDHashFormatString = @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
|
||||
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x";
|
||||
|
||||
const uint8_t *buf = targetProc->cdhash;
|
||||
cdhash = [[NSString alloc] initWithFormat:kCDHashFormatString, buf[0], buf[1], buf[2], buf[3],
|
||||
buf[4], buf[5], buf[6], buf[7], buf[8], buf[9],
|
||||
buf[10], buf[11], buf[12], buf[13], buf[14],
|
||||
buf[15], buf[16], buf[17], buf[18], buf[19]];
|
||||
}
|
||||
}
|
||||
|
||||
return [self decisionForFileInfo:fileInfo
|
||||
cdhash:cdhash
|
||||
fileSHA256:nil
|
||||
certificateSHA256:nil
|
||||
teamID:teamID
|
||||
@@ -292,10 +339,7 @@
|
||||
|
||||
// Used by `$ santactl fileinfo`.
|
||||
- (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID {
|
||||
identifiers:(nonnull SNTRuleIdentifiers *)identifiers {
|
||||
MOLCodesignChecker *csInfo;
|
||||
NSError *error;
|
||||
|
||||
@@ -310,10 +354,11 @@
|
||||
}
|
||||
|
||||
return [self decisionForFileInfo:fileInfo
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
cdhash:identifiers.cdhash
|
||||
fileSHA256:identifiers.binarySHA256
|
||||
certificateSHA256:identifiers.certificateSHA256
|
||||
teamID:identifiers.teamID
|
||||
signingID:identifiers.signingID
|
||||
isProdSignedCallback:^BOOL {
|
||||
if (csInfo) {
|
||||
// Development OID values defined by Apple and used by the Security Framework
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
#import "Source/santad/SNTCompilerController.h"
|
||||
#import "Source/santad/SNTExecutionController.h"
|
||||
#import "Source/santad/SNTNotificationQueue.h"
|
||||
@@ -47,6 +48,7 @@ void SantadMain(
|
||||
SNTNotificationQueue* notifier_queue, SNTSyncdQueue* syncd_queue,
|
||||
SNTExecutionController* exec_controller,
|
||||
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> prefix_tree,
|
||||
std::shared_ptr<santa::santad::TTYWriter> tty_writer);
|
||||
std::shared_ptr<santa::santad::TTYWriter> tty_writer,
|
||||
std::shared_ptr<santa::santad::process_tree::ProcessTree> process_tree);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -82,7 +82,8 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
|
||||
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
|
||||
SNTExecutionController *exec_controller,
|
||||
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> prefix_tree,
|
||||
std::shared_ptr<TTYWriter> tty_writer) {
|
||||
std::shared_ptr<TTYWriter> tty_writer,
|
||||
std::shared_ptr<santa::santad::process_tree::ProcessTree> process_tree) {
|
||||
SNTConfigurator *configurator = [SNTConfigurator configurator];
|
||||
|
||||
SNTDaemonControlController *dc =
|
||||
@@ -123,7 +124,8 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
|
||||
enricher:enricher
|
||||
compilerController:compiler_controller
|
||||
authResultCache:auth_result_cache
|
||||
prefixTree:prefix_tree];
|
||||
prefixTree:prefix_tree
|
||||
processTree:process_tree];
|
||||
|
||||
SNTEndpointSecurityAuthorizer *authorizer_client =
|
||||
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:esapi
|
||||
@@ -443,6 +445,13 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
|
||||
// of the SNTKVOManager objects it contains.
|
||||
(void)kvoObservers;
|
||||
|
||||
if (process_tree) {
|
||||
if (absl::Status status = process_tree->Backfill(); !status.ok()) {
|
||||
std::string err = status.ToString();
|
||||
LOGE(@"Failed to backfill process tree: %@", @(err.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
// IMPORTANT: ES will hold up third party execs until early boot clients make
|
||||
// their first subscription. Ensuring the `Authorizer` client is enabled first
|
||||
// means that the AUTH EXEC event is subscribed first and Santa can apply
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
#import "Source/santad/SNTCompilerController.h"
|
||||
#import "Source/santad/SNTExecutionController.h"
|
||||
#import "Source/santad/SNTNotificationQueue.h"
|
||||
@@ -58,7 +59,8 @@ class SantadDeps {
|
||||
SNTExecutionController *exec_controller,
|
||||
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>>
|
||||
prefix_tree,
|
||||
std::shared_ptr<santa::santad::TTYWriter> tty_writer);
|
||||
std::shared_ptr<santa::santad::TTYWriter> tty_writer,
|
||||
std::shared_ptr<process_tree::ProcessTree> process_tree);
|
||||
|
||||
std::shared_ptr<santa::santad::event_providers::AuthResultCache>
|
||||
AuthResultCache();
|
||||
@@ -77,6 +79,7 @@ class SantadDeps {
|
||||
SNTExecutionController *ExecController();
|
||||
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> PrefixTree();
|
||||
std::shared_ptr<santa::santad::TTYWriter> TTYWriter();
|
||||
std::shared_ptr<process_tree::ProcessTree> ProcessTree();
|
||||
|
||||
private:
|
||||
std::shared_ptr<
|
||||
@@ -97,6 +100,7 @@ class SantadDeps {
|
||||
SNTExecutionController *exec_controller_;
|
||||
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> prefix_tree_;
|
||||
std::shared_ptr<santa::santad::TTYWriter> tty_writer_;
|
||||
std::shared_ptr<process_tree::ProcessTree> process_tree_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
#include "Source/santad/DataLayer/WatchItems.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
#import "Source/santad/SNTDatabaseController.h"
|
||||
#include "Source/santad/SNTDecisionCache.h"
|
||||
#include "Source/santad/TTYWriter.h"
|
||||
@@ -154,10 +155,25 @@ std::unique_ptr<SantadDeps> SantadDeps::Create(SNTConfigurator *configurator,
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
std::shared_ptr<process_tree::ProcessTree> process_tree;
|
||||
std::vector<std::unique_ptr<process_tree::Annotator>> annotators;
|
||||
|
||||
for (NSString *annotation in [configurator enabledProcessAnnotations]) {
|
||||
// TODO(nickmg): add annotation name switch
|
||||
(void)annotation;
|
||||
}
|
||||
|
||||
auto tree_status = process_tree::CreateTree(std::move(annotators));
|
||||
if (!tree_status.ok()) {
|
||||
LOGE(@"Failed to create process tree: %@", @(tree_status.status().ToString().c_str()));
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
process_tree = *tree_status;
|
||||
|
||||
return std::make_unique<SantadDeps>(
|
||||
esapi, std::move(logger), std::move(metrics), std::move(watch_items),
|
||||
std::move(auth_result_cache), control_connection, compiler_controller, notifier_queue,
|
||||
syncd_queue, exec_controller, prefix_tree, std::move(tty_writer));
|
||||
syncd_queue, exec_controller, prefix_tree, std::move(tty_writer), std::move(process_tree));
|
||||
}
|
||||
|
||||
SantadDeps::SantadDeps(
|
||||
@@ -167,12 +183,12 @@ SantadDeps::SantadDeps(
|
||||
MOLXPCConnection *control_connection, SNTCompilerController *compiler_controller,
|
||||
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
|
||||
SNTExecutionController *exec_controller, std::shared_ptr<::PrefixTree<Unit>> prefix_tree,
|
||||
std::shared_ptr<::TTYWriter> tty_writer)
|
||||
std::shared_ptr<::TTYWriter> tty_writer, std::shared_ptr<process_tree::ProcessTree> process_tree)
|
||||
: esapi_(std::move(esapi)),
|
||||
logger_(std::move(logger)),
|
||||
metrics_(std::move(metrics)),
|
||||
watch_items_(std::move(watch_items)),
|
||||
enricher_(std::make_shared<::Enricher>()),
|
||||
enricher_(std::make_shared<::Enricher>(process_tree)),
|
||||
auth_result_cache_(std::move(auth_result_cache)),
|
||||
control_connection_(control_connection),
|
||||
compiler_controller_(compiler_controller),
|
||||
@@ -180,7 +196,8 @@ SantadDeps::SantadDeps(
|
||||
syncd_queue_(syncd_queue),
|
||||
exec_controller_(exec_controller),
|
||||
prefix_tree_(prefix_tree),
|
||||
tty_writer_(std::move(tty_writer)) {}
|
||||
tty_writer_(std::move(tty_writer)),
|
||||
process_tree_(std::move(process_tree)) {}
|
||||
|
||||
std::shared_ptr<::AuthResultCache> SantadDeps::AuthResultCache() {
|
||||
return auth_result_cache_;
|
||||
@@ -233,4 +250,8 @@ std::shared_ptr<::TTYWriter> SantadDeps::TTYWriter() {
|
||||
return tty_writer_;
|
||||
}
|
||||
|
||||
std::shared_ptr<process_tree::ProcessTree> SantadDeps::ProcessTree() {
|
||||
return process_tree_;
|
||||
}
|
||||
|
||||
} // namespace santa::santad
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
#import <dispatch/dispatch.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <cctype>
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
@@ -30,6 +32,7 @@
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#import "Source/santad/Metrics.h"
|
||||
#import "Source/santad/SNTDatabaseController.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
@@ -38,12 +41,50 @@
|
||||
using santa::santad::SantadDeps;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
static int HexCharToInt(char hex) {
|
||||
if (hex >= '0' && hex <= '9') {
|
||||
return hex - '0';
|
||||
} else if (hex >= 'A' && hex <= 'F') {
|
||||
return hex - 'A' + 10;
|
||||
} else if (hex >= 'a' && hex <= 'f') {
|
||||
return hex - 'a' + 10;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static void SetBinaryDataFromHexString(const char *hexStr, uint8_t *buf, size_t bufLen) {
|
||||
assert(hexStr != NULL);
|
||||
size_t hexStrLen = strlen(hexStr);
|
||||
assert(hexStrLen > 0);
|
||||
assert(hexStrLen % 2 == 0);
|
||||
assert(hexStrLen / 2 == bufLen);
|
||||
|
||||
for (size_t i = 0; i < hexStrLen; i += 2) {
|
||||
int upper = HexCharToInt(hexStr[i]);
|
||||
int lower = HexCharToInt(hexStr[i + 1]);
|
||||
|
||||
assert(upper != -1);
|
||||
assert(lower != -1);
|
||||
|
||||
buf[i / 2] = (uint8_t)(upper << 4) | lower;
|
||||
}
|
||||
}
|
||||
|
||||
static NSString *const 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 *kAllowedTeamID = "TJNVEKW352";
|
||||
static const char *kAllowedCDHash = "dedebf2eac732d873008b17b3e44a56599dd614b";
|
||||
static const char *kBlockedCDHash = "7218eddfee4d3eba4873dedf22d1391d79aea25f";
|
||||
|
||||
@interface SNTEndpointSecurityClient (Testing)
|
||||
@property(nonatomic) double defaultBudget;
|
||||
@property(nonatomic) int64_t minAllowedHeadroom;
|
||||
@property(nonatomic) int64_t maxAllowedHeadroom;
|
||||
@end
|
||||
|
||||
@interface SantadTest : XCTestCase
|
||||
@property id mockSNTDatabaseController;
|
||||
@@ -85,9 +126,8 @@ static const char *kAllowedTeamID = "TJNVEKW352";
|
||||
OCMStub([mockConfigurator failClosed]).andReturn(NO);
|
||||
OCMStub([mockConfigurator fileAccessPolicyUpdateIntervalSec]).andReturn(600);
|
||||
|
||||
NSString *baseTestPath = @"santa/Source/santad/testdata/binaryrules";
|
||||
NSString *testPath = [NSString pathWithComponents:@[
|
||||
[[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], baseTestPath
|
||||
[[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testBinariesPath
|
||||
]];
|
||||
|
||||
OCMStub([self.mockSNTDatabaseController databasePath]).andReturn(testPath);
|
||||
@@ -114,16 +154,19 @@ static const char *kAllowedTeamID = "TJNVEKW352";
|
||||
NSString *binaryPath =
|
||||
[[NSString pathWithComponents:@[ testPath, binaryName ]] stringByResolvingSymlinksInPath];
|
||||
struct stat fileStat;
|
||||
lstat(binaryPath.UTF8String, &fileStat);
|
||||
XCTAssertEqual(lstat(binaryPath.UTF8String, &fileStat), 0);
|
||||
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
|
||||
proc.codesigning_flags = CS_SIGNED | CS_VALID | CS_HARD | CS_KILL;
|
||||
|
||||
// Set a 6.5 second deadline for the message and clamp deadline headroom to 5
|
||||
// seconds. This means there is a 1.5 second leeway given for the processing block
|
||||
// to finish its tasks and release the `Message`. This will add about 1 second
|
||||
// to the run time of each test case since each one must wait for the
|
||||
// deadline block to run and release the message.
|
||||
authClient.minAllowedHeadroom = 5 * NSEC_PER_SEC;
|
||||
authClient.maxAllowedHeadroom = 5 * NSEC_PER_SEC;
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth, 6500);
|
||||
esMsg.event.exec.target = &proc;
|
||||
|
||||
@@ -360,6 +403,58 @@ static const char *kAllowedTeamID = "TJNVEKW352";
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithCDHashBlockRuleIsBlockedInLockdownMode {
|
||||
[self checkBinaryExecution:@"banned_cdhash"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeLockdown
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateBlockCDHash;
|
||||
}
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
SetBinaryDataFromHexString(kBlockedCDHash, msg->event.exec.target->cdhash,
|
||||
sizeof(msg->event.exec.target->cdhash));
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithCDHashBlockRuleIsBlockedInMonitorMode {
|
||||
[self checkBinaryExecution:@"banned_cdhash"
|
||||
wantResult:ES_AUTH_RESULT_DENY
|
||||
clientMode:SNTClientModeMonitor
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateBlockCDHash;
|
||||
}
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
SetBinaryDataFromHexString(kBlockedCDHash, msg->event.exec.target->cdhash,
|
||||
sizeof(msg->event.exec.target->cdhash));
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithCDHashAllowRuleIsAllowedInMonitorMode {
|
||||
[self checkBinaryExecution:@"allowed_cdhash"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateAllowCDHash;
|
||||
}
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
SetBinaryDataFromHexString(kAllowedCDHash, msg->event.exec.target->cdhash,
|
||||
sizeof(msg->event.exec.target->cdhash));
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithCDHashAllowRuleIsAllowedInLockdownMode {
|
||||
[self checkBinaryExecution:@"allowed_cdhash"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
clientMode:SNTClientModeMonitor
|
||||
cdValidator:^BOOL(SNTCachedDecision *cd) {
|
||||
return cd.decision == SNTEventStateAllowCDHash;
|
||||
}
|
||||
messageSetup:^(es_message_t *msg) {
|
||||
SetBinaryDataFromHexString(kAllowedCDHash, msg->event.exec.target->cdhash,
|
||||
sizeof(msg->event.exec.target->cdhash));
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testBinaryWithSHA256AllowRuleAndBlockedTeamIDRuleIsAllowedInLockdownMode {
|
||||
[self checkBinaryExecution:@"banned_teamid_allowed_binary"
|
||||
wantResult:ES_AUTH_RESULT_ALLOW
|
||||
|
||||
@@ -153,7 +153,7 @@ int main(int argc, char *argv[]) {
|
||||
SantadMain(deps->ESAPI(), deps->Logger(), deps->Metrics(), deps->WatchItems(), deps->Enricher(),
|
||||
deps->AuthResultCache(), deps->ControlConnection(), deps->CompilerController(),
|
||||
deps->NotifierQueue(), deps->SyncdQueue(), deps->ExecController(),
|
||||
deps->PrefixTree(), deps->TTYWriter());
|
||||
deps->PrefixTree(), deps->TTYWriter(), deps->ProcessTree());
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
BIN
Source/santad/testdata/binaryrules/allowed_cdhash
vendored
Executable file
BIN
Source/santad/testdata/binaryrules/allowed_cdhash
vendored
Executable file
Binary file not shown.
BIN
Source/santad/testdata/binaryrules/banned_cdhash
vendored
Executable file
BIN
Source/santad/testdata/binaryrules/banned_cdhash
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.
@@ -104,6 +104,7 @@
|
||||
case SNTEventStateAllowScope: ADDKEY(newEvent, kDecision, kDecisionAllowScope); break;
|
||||
case SNTEventStateAllowTeamID: ADDKEY(newEvent, kDecision, kDecisionAllowTeamID); break;
|
||||
case SNTEventStateAllowSigningID: ADDKEY(newEvent, kDecision, kDecisionAllowSigningID); break;
|
||||
case SNTEventStateAllowCDHash: ADDKEY(newEvent, kDecision, kDecisionAllowCDHash); break;
|
||||
case SNTEventStateBlockUnknown: ADDKEY(newEvent, kDecision, kDecisionBlockUnknown); break;
|
||||
case SNTEventStateBlockBinary: ADDKEY(newEvent, kDecision, kDecisionBlockBinary); break;
|
||||
case SNTEventStateBlockCertificate:
|
||||
@@ -112,6 +113,7 @@
|
||||
case SNTEventStateBlockScope: ADDKEY(newEvent, kDecision, kDecisionBlockScope); break;
|
||||
case SNTEventStateBlockTeamID: ADDKEY(newEvent, kDecision, kDecisionBlockTeamID); break;
|
||||
case SNTEventStateBlockSigningID: ADDKEY(newEvent, kDecision, kDecisionBlockSigningID); break;
|
||||
case SNTEventStateBlockCDHash: ADDKEY(newEvent, kDecision, kDecisionBlockCDHash); break;
|
||||
case SNTEventStateBundleBinary:
|
||||
ADDKEY(newEvent, kDecision, kDecisionBundleBinary);
|
||||
[newEvent removeObjectForKey:kExecutionTime];
|
||||
@@ -155,6 +157,7 @@
|
||||
newEvent[kSigningChain] = signingChain;
|
||||
ADDKEY(newEvent, kTeamID, event.teamID);
|
||||
ADDKEY(newEvent, kSigningID, event.signingID);
|
||||
ADDKEY(newEvent, kCDHash, event.cdhash);
|
||||
|
||||
return newEvent;
|
||||
#undef ADDKEY
|
||||
|
||||
@@ -88,16 +88,14 @@ The following table expands upon the above logic to list most of the permutation
|
||||
|
||||
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
|
||||
|
||||
// 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 signingID) {
|
||||
requestDict[kBinaryRuleCount] = @(binary);
|
||||
requestDict[kCertificateRuleCount] = @(certificate);
|
||||
requestDict[kCompilerRuleCount] = @(compiler);
|
||||
requestDict[kTransitiveRuleCount] = @(transitive);
|
||||
requestDict[kTeamIDRuleCount] = @(teamID);
|
||||
requestDict[kSigningIDRuleCount] = @(signingID);
|
||||
[rop databaseRuleCounts:^(struct RuleCounts counts) {
|
||||
requestDict[kBinaryRuleCount] = @(counts.binary);
|
||||
requestDict[kCertificateRuleCount] = @(counts.certificate);
|
||||
requestDict[kCompilerRuleCount] = @(counts.compiler);
|
||||
requestDict[kTransitiveRuleCount] = @(counts.transitive);
|
||||
requestDict[kTeamIDRuleCount] = @(counts.teamID);
|
||||
requestDict[kSigningIDRuleCount] = @(counts.signingID);
|
||||
requestDict[kCDHashRuleCount] = @(counts.cdhash);
|
||||
}];
|
||||
|
||||
[rop clientMode:^(SNTClientMode cm) {
|
||||
|
||||
@@ -147,14 +147,9 @@
|
||||
}
|
||||
|
||||
- (void)setupDefaultDaemonConnResponses {
|
||||
struct RuleCounts ruleCounts = {0};
|
||||
OCMStub([self.daemonConnRop
|
||||
databaseRuleCounts:([OCMArg invokeBlockWithArgs:OCMOCK_VALUE(0), // binary
|
||||
OCMOCK_VALUE(0), // cert
|
||||
OCMOCK_VALUE(0), // compiler
|
||||
OCMOCK_VALUE(0), // transitive
|
||||
OCMOCK_VALUE(0), // teamID
|
||||
OCMOCK_VALUE(0), // signingID
|
||||
nil])]);
|
||||
databaseRuleCounts:([OCMArg invokeBlockWithArgs:OCMOCK_VALUE(ruleCounts), nil])]);
|
||||
OCMStub([self.daemonConnRop
|
||||
syncTypeRequired:([OCMArg invokeBlockWithArgs:OCMOCK_VALUE(SNTSyncTypeNormal), nil])]);
|
||||
OCMStub([self.daemonConnRop
|
||||
@@ -350,29 +345,31 @@
|
||||
- (void)testPreflightDatabaseCounts {
|
||||
SNTSyncPreflight *sut = [[SNTSyncPreflight alloc] initWithState:self.syncState];
|
||||
|
||||
int64_t bin = 5;
|
||||
int64_t cert = 8;
|
||||
int64_t compiler = 2;
|
||||
int64_t transitive = 19;
|
||||
int64_t teamID = 3;
|
||||
int64_t signingID = 123;
|
||||
struct RuleCounts ruleCounts = {
|
||||
.cdhash = 11,
|
||||
.binary = 5,
|
||||
.certificate = 8,
|
||||
.compiler = 2,
|
||||
.transitive = 19,
|
||||
.teamID = 3,
|
||||
.signingID = 123,
|
||||
};
|
||||
|
||||
OCMStub([self.daemonConnRop
|
||||
databaseRuleCounts:([OCMArg invokeBlockWithArgs:OCMOCK_VALUE(bin), OCMOCK_VALUE(cert),
|
||||
OCMOCK_VALUE(compiler),
|
||||
OCMOCK_VALUE(transitive), OCMOCK_VALUE(teamID),
|
||||
OCMOCK_VALUE(signingID), nil])]);
|
||||
databaseRuleCounts:([OCMArg invokeBlockWithArgs:OCMOCK_VALUE(ruleCounts), nil])]);
|
||||
|
||||
[self stubRequestBody:nil
|
||||
response:nil
|
||||
error:nil
|
||||
validateBlock:^BOOL(NSURLRequest *req) {
|
||||
NSDictionary *requestDict = [self dictFromRequest:req];
|
||||
XCTAssertEqualObjects(requestDict[kBinaryRuleCount], @(5));
|
||||
XCTAssertEqualObjects(requestDict[kCertificateRuleCount], @(8));
|
||||
XCTAssertEqualObjects(requestDict[kCompilerRuleCount], @(2));
|
||||
XCTAssertEqualObjects(requestDict[kTransitiveRuleCount], @(19));
|
||||
XCTAssertEqualObjects(requestDict[kTeamIDRuleCount], @(3));
|
||||
XCTAssertEqualObjects(requestDict[kSigningIDRuleCount], @(123));
|
||||
XCTAssertEqualObjects(requestDict[kCDHashRuleCount], @(ruleCounts.cdhash));
|
||||
XCTAssertEqualObjects(requestDict[kBinaryRuleCount], @(ruleCounts.binary));
|
||||
XCTAssertEqualObjects(requestDict[kCertificateRuleCount], @(ruleCounts.certificate));
|
||||
XCTAssertEqualObjects(requestDict[kCompilerRuleCount], @(ruleCounts.compiler));
|
||||
XCTAssertEqualObjects(requestDict[kTransitiveRuleCount], @(ruleCounts.transitive));
|
||||
XCTAssertEqualObjects(requestDict[kTeamIDRuleCount], @(ruleCounts.teamID));
|
||||
XCTAssertEqualObjects(requestDict[kSigningIDRuleCount], @(ruleCounts.signingID));
|
||||
return YES;
|
||||
}];
|
||||
|
||||
@@ -387,14 +384,9 @@
|
||||
response:(NSDictionary *)resp {
|
||||
SNTSyncPreflight *sut = [[SNTSyncPreflight alloc] initWithState:self.syncState];
|
||||
|
||||
struct RuleCounts ruleCounts = {0};
|
||||
OCMStub([self.daemonConnRop
|
||||
databaseRuleCounts:([OCMArg invokeBlockWithArgs:OCMOCK_VALUE(0), // binary
|
||||
OCMOCK_VALUE(0), // cert
|
||||
OCMOCK_VALUE(0), // compiler
|
||||
OCMOCK_VALUE(0), // transitive
|
||||
OCMOCK_VALUE(0), // teamID
|
||||
OCMOCK_VALUE(0), // signingID
|
||||
nil])]);
|
||||
databaseRuleCounts:([OCMArg invokeBlockWithArgs:OCMOCK_VALUE(ruleCounts), nil])]);
|
||||
OCMStub([self.daemonConnRop
|
||||
clientMode:([OCMArg invokeBlockWithArgs:OCMOCK_VALUE(SNTClientModeMonitor), nil])]);
|
||||
OCMStub([self.daemonConnRop
|
||||
@@ -601,6 +593,7 @@
|
||||
|
||||
XCTAssertEqualObjects(event[kTeamID], @"012345678910");
|
||||
XCTAssertEqualObjects(event[kSigningID], @"signing.id");
|
||||
XCTAssertEqualObjects(event[kCDHash], @"abc123");
|
||||
|
||||
event = events[1];
|
||||
XCTAssertEqualObjects(event[kFileName], @"hub");
|
||||
|
||||
@@ -101,6 +101,11 @@
|
||||
<key>CF$UID</key>
|
||||
<integer>40</integer>
|
||||
</dict>
|
||||
<key>cdhash</key>
|
||||
<dict>
|
||||
<key>CF$UID</key>
|
||||
<integer>41</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<integer>14887</integer>
|
||||
<string>ff98fa0c0a1095fedcbe4d388a9760e71399a5c3c017a847ffa545663b57929a</string>
|
||||
@@ -405,6 +410,7 @@
|
||||
</dict>
|
||||
<string>012345678910</string>
|
||||
<string>signing.id</string>
|
||||
<string>abc123</string>
|
||||
</array>
|
||||
<key>$top</key>
|
||||
<dict>
|
||||
|
||||
@@ -73,6 +73,7 @@ JSON blob. Here is an example of Firefox being blocked and sent for upload:
|
||||
],
|
||||
"team_id": "43AQ936H96",
|
||||
"signing_id": "org.mozilla.firefox",
|
||||
"cdhash": "ac14c49901a9cd05ff7bceea122f534d3c6c6ab7",
|
||||
"file_bundle_name": "Firefox",
|
||||
"executing_user": "bur",
|
||||
"ppid": 1,
|
||||
|
||||
@@ -9,11 +9,21 @@ parent: Concepts
|
||||
Rules provide the primary evaluation mechanism for allowing and blocking
|
||||
binaries with Santa on macOS.
|
||||
|
||||
### CDHash Rules
|
||||
|
||||
CDHash rules use a binary's code directory hash as an identifier. This is the
|
||||
most specific rule in Santa. The code directory hash identifies a specific
|
||||
version of a program, similar to a file hash. Note that the operating system
|
||||
evaluates the cdhash lazily, only verifying pages of code when they're mapped
|
||||
in. This means that it is possible for a file hash to change, but a binary could
|
||||
still execute as long as modified pages are not mapped in. Santa only considers
|
||||
CDHash rules for processes that have `CS_KILL` or `CS_HARD` codesigning flags
|
||||
set to ensure that a process will be killed if the CDHash was tampered with
|
||||
(assuming the system has SIP enabled).
|
||||
|
||||
### Binary Rules
|
||||
|
||||
Binary rules use the SHA-256 hash of the entire binary as an identifier. This is
|
||||
the most specific rule in Santa. Even a small change in the binary will alter
|
||||
the SHA-256 hash, invalidating the rule.
|
||||
Binary rules use the SHA-256 hash of the entire binary file as an identifier.
|
||||
|
||||
### Signing ID Rules
|
||||
|
||||
@@ -105,7 +115,7 @@ specific to least specific:
|
||||
```
|
||||
Most Specific Least Specific
|
||||
|
||||
Binary --> Signing ID --> Certificate --> Team ID
|
||||
CDHash → Binary → Signing ID → Certificate → Team ID
|
||||
```
|
||||
|
||||
If no rules are found that apply, scopes are then searched. See the
|
||||
|
||||
@@ -23,7 +23,7 @@ also known as mobileconfig files, which are in an Apple-specific XML format.
|
||||
| Key | Value Type | Description |
|
||||
| ---------------------------------- | ---------- | ---------------------------------------- |
|
||||
| ClientMode\* | Integer | 1 = MONITOR, 2 = LOCKDOWN, defaults to MONITOR |
|
||||
| FailClosed | Bool | If true and the ClientMode is LOCKDOWN: execution will be denied when there is an error reading or processing an executable file. Defaults to false. |
|
||||
| FailClosed | Bool | If true and the ClientMode is LOCKDOWN: execution will be denied when there is an error reading or processing an executable file and when Santa has to make a default response just prior to deadlines expiring. Defaults to false. |
|
||||
| FileChangesRegex\* | String | The regex of paths to log file changes. Regexes are specified in ICU format. |
|
||||
| AllowedPathRegex\* | String | A regex to allow if the binary, certificate, or Team ID scopes did not allow/block execution. Regexes are specified in ICU format. |
|
||||
| BlockedPathRegex\* | String | A regex to block if the binary, certificate, or Team ID scopes did not allow/block an execution. Regexes are specified in ICU format. |
|
||||
@@ -114,6 +114,17 @@ them to. The following sequences will be replaced in the final URL:
|
||||
|
||||
For example: `https://sync-server-hostname/%machine_id%/%file_sha%`
|
||||
|
||||
### AllowedPathRegex/BlockedPathRegex
|
||||
|
||||
These regexes can be used to allow/block binaries based on the executable path.
|
||||
We strongly discourage the use of this as it can be relatively trivial to bypass
|
||||
but there are some circumstances where it is the only option.
|
||||
|
||||
It's important to note that the path matched against these regexes is the full
|
||||
absolute path of the binary file. Symlinks in the path will have already been
|
||||
followed by the time Santa processes the execution and matches against the
|
||||
regex.
|
||||
|
||||
### Static Rules
|
||||
|
||||
Static rules are rules that are defined inline in the Santa configuration. These
|
||||
|
||||
@@ -97,6 +97,8 @@ The request consists of the following JSON keys:
|
||||
| compiler_rule_count | NO | int | Number of compiler rules the client has time of sync |
|
||||
| transitive_rule_count | NO | int | Number of transitive rules the client has at the time of sync |
|
||||
| teamid_rule_count | NO | int | Number of TeamID rules the client has at the time of sync | 24 |
|
||||
| signingid_rule_count | NO | int | Number of SigningID rules the client has at the time of sync | 11 |
|
||||
| cdhash_rule_count | NO | int | Number of CDHash rules the client has at the time of sync | 22 |
|
||||
| client_mode | YES | string | The mode the client is operating in, either "LOCKDOWN" or "MONITOR" | LOCKDOWN |
|
||||
| request_clean_sync | NO | bool | The client has requested a clean sync of its rules from the server | true |
|
||||
|
||||
@@ -114,6 +116,8 @@ The request consists of the following JSON keys:
|
||||
"primary_user" : "markowsky",
|
||||
"certificate_rule_count" : 2364,
|
||||
"teamid_rule_count" : 0,
|
||||
"signingid_rule_count" : 12,
|
||||
"cdhash_rule_count" : 34,
|
||||
"os_build" : "21F5048e",
|
||||
"transitive_rule_count" : 0,
|
||||
"os_version" : "12.4",
|
||||
@@ -208,7 +212,7 @@ sequenceDiagram
|
||||
| execution_time | NO | float64 | Unix timestamp of when the execution occurred | 23344234232 |
|
||||
| loggedin_users | NO | list of strings | List of usernames logged in according to utmp | ["markowsky"] |
|
||||
| current_sessions | NO | list of strings | List of user sessions | ["markowsky@console", "markowsky@ttys000"] |
|
||||
| decision | YES | string | The decision Santa made for this binary, BUNDLE_BINARY is used to preemptively report binaries in a bundle. **Must be one of the examples** | "ALLOW_BINARY", "ALLOW_CERTIFICATE", "ALLOW_SCOPE", "ALLOW_TEAMID", "ALLOW_UNKNOWN", "BLOCK_BINARY", "BLOCK_CERTIFICATE", "BLOCK_SCOPE", "BLOCK_TEAMID", "BLOCK_UNKNOWN", "BUNDLE_BINARY" |
|
||||
| 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_SIGNINGID", "ALLOW_CDHASH" "ALLOW_UNKNOWN", "BLOCK_BINARY", "BLOCK_CERTIFICATE", "BLOCK_SCOPE", "BLOCK_TEAMID", "BLOCK_SIGNINGID", "BLOCK_CDHASH", "BLOCK_UNKNOWN", "BUNDLE_BINARY" |
|
||||
| file_bundle_id | NO | string | The executable's containing bundle's identifier as specified in the Info.plist | "com.apple.safari" |
|
||||
| file_bundle_path | NO | string | The path that the bundle resids in | /Applications/Santa.app |
|
||||
| file_bundle_executable_rel_path | NO | string | The relative path of the binary within the Bundle | "Contents/MacOS/AppName" |
|
||||
@@ -228,6 +232,7 @@ sequenceDiagram
|
||||
| signing_chain | NO | list of signing chain objects | Certs used to code sign the executable | See next section |
|
||||
| signing_id | NO | string | Signing ID of the binary that was executed | "EQHXZ8M8AV:com.google.Chrome" |
|
||||
| team_id | NO | string | Team ID of the binary that was executed | "EQHXZ8M8AV" |
|
||||
| cdhash | NO | string | CDHash of the binary that was executed | "dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a" |
|
||||
|
||||
#### Signing Chain Objects
|
||||
|
||||
@@ -296,7 +301,8 @@ sequenceDiagram
|
||||
"markowsky@ttys003"
|
||||
],
|
||||
"team_id": "EQHXZ8M8AV",
|
||||
"signing_id": "EQHXZ8M8AV:com.google.santa"
|
||||
"signing_id": "EQHXZ8M8AV:com.google.santa",
|
||||
"cdhash": "dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a"
|
||||
}]
|
||||
}
|
||||
```
|
||||
@@ -380,9 +386,9 @@ downloading if the rules need to be downloaded in multiple batches.
|
||||
|
||||
| Key | Required | Type | Meaning | Example Value |
|
||||
|---|---|---|---|---|
|
||||
| identifier | YES | string | The attribute of the binary the rule should match on e.g. the team ID of a binary or sha256 hash value | "ff2a7daa4c25cbd5b057e4471c6a22aba7d154dadfb5cce139c37cf795f41c9c" |
|
||||
| identifier | YES | string | The attribute of the binary the rule should match on e.g. the signing ID, team ID, or CDHash of a binary or sha256 hash value | "ff2a7daa4c25cbd5b057e4471c6a22aba7d154dadfb5cce139c37cf795f41c9c" |
|
||||
| policy | YES | string | Identifies the action to perform in response to the rule matching (must be one of the examples) | "ALLOWLIST","ALLOWLIST_COMPILER", "BLOCKLIST", "REMOVE", "SILENT_BLOCKLIST" |
|
||||
| rule\_type | YES | string | Identifies the type of rule (must be one of the examples) | "BINARY", "CERTIFICATE", "SIGNINGID", "TEAMID" |
|
||||
| rule\_type | YES | string | Identifies the type of rule (must be one of the examples) | "BINARY", "CERTIFICATE", "SIGNINGID", "TEAMID", "CDHASH" |
|
||||
| custom\_msg | NO | string | A custom message to display when the rule matches | "Hello" |
|
||||
| custom\_url | NO | string | A custom URL to use for the open button when the rule matches | http://lmgtfy.app/?q=dont+download+malware |
|
||||
| creation\_time | NO | float64 | Time the rule was created | 1573543803.349378 |
|
||||
|
||||
@@ -15,7 +15,7 @@ The project and the latest release is available on [**GitHub**](https://github.c
|
||||
|
||||
* [**Multiple modes:**](concepts/mode.md) In the default `MONITOR` mode, all binaries except those marked as blocked will be allowed to run, whilst being logged and recorded in the events database. In `LOCKDOWN` mode, only listed binaries are allowed to run.
|
||||
* [**Event logging:**](concepts/events.md) All binary launches are logged. When in either mode, all unknown or denied binaries are stored in the database to enable later aggregation.
|
||||
* [**Several supported rule types:**](concepts/rules.md) Executions can be allowed or denied by specifying rules based on several attributes. The supported rule types, in order of highest to lowest precedence are: binary hash, Signing ID, certificate hash, or Team ID. Since multiple rules can apply to a given binary, Santa will apply the rule with the highest precedence (i.e. you could use a Team ID rule to allow all binaries from some organization, but also add a Signing ID rule to deny a specific binary). Rules based on code signature properties (Signing ID, certificate hash, and Team ID) only apply if a bianry's signature validates correctly.
|
||||
* [**Several supported rule types:**](concepts/rules.md) Executions can be allowed or denied by specifying rules based on several attributes. The supported rule types, in order of highest to lowest precedence are: CDHash, binary hash, Signing ID, certificate hash, or Team ID. Since multiple rules can apply to a given binary, Santa will apply the rule with the highest precedence (i.e. you could use a Team ID rule to allow all binaries from some organization, but also add a Signing ID rule to deny a specific binary). Rules based on code signature properties (Signing ID, certificate hash, and Team ID) only apply if a bianry's signature validates correctly.
|
||||
* **Path-based rules (via NSRegularExpression/ICU):** Binaries can be allowed/blocked based on the path they are launched from by matching against a configurable regex.
|
||||
* [**Failsafe cert rules:**](concepts/rules.md#built-in-rules) You cannot put in a deny rule that would block the certificate used to sign launchd, a.k.a. pid 1, and therefore all components used in macOS. The binaries in every OS update (and in some cases entire new versions) are therefore automatically allowed. This does not affect binaries from Apple's App Store, which use various certs that change regularly for common apps. Likewise, you cannot block Santa itself.
|
||||
* [**Components validate each other:**](binaries/index.md) Each of the components (the daemons, the GUI agent, and the command-line utility) communicate with each other using XPC and check that their signing certificates are identical before any communication is accepted.
|
||||
|
||||
Reference in New Issue
Block a user