Compare commits

...

24 Commits

Author SHA1 Message Date
Nick Gregory
57fc2b0253 Add missing EndpointSecurity dylib (#1315) 2024-03-25 13:41:20 -04:00
Nick Gregory
262adfecbd Fix BUILD deps (#1314) 2024-03-25 13:19:13 -04:00
Jason McCandless
1606657bb3 Add CDHash to rule evaluation order doc. (#1313) 2024-03-22 18:13:58 -04:00
Matt W
b379819cfa Overrides disabled when running tests unless explicitly enabled (#1312)
* Emit a log warning when overrides were applied

* Overrides now disabled in tests unless explicitly enabled

* Remove log message. Check for xctest instead of bazel env vars.

* Typo
2024-03-22 16:44:45 -04:00
Pete Markowsky
b9f6005411 Fix: Do not flush authcache when receiving duplicate block rules from the sync service (#1310)
* Change the behavior of addedRulesShouldFlushDecisionCache to flush when 1000 non-allowlist rules are added or a remove rule is encountered or any new non-allowlist rules are added

* Add tests for cache flushing behavior.
2024-03-22 11:24:42 -04:00
Russell Hancox
e31aa5cf39 Tests: Fix SNTRuleTableTest in the presence of local static rules (#1311) 2024-03-19 18:06:39 -04:00
Nick Gregory
77d191ae26 ProcessTree: integrate process tree throughout the event processing lifecycle (3/4) (#1281)
* process annotations: thread the tree through santa

* Update enricher to read annotations from the ProcessTree

* rebase changes

* add configuration for annotations, disabling the tree entirely if none are enabled

* lingering build dep

* use tree factory constructor

* fix configurator

* build fixes

* rebase fixes

* fix tests

* review comments

* lint

* english hard

* record metrics even when event only used for process tree
2024-03-14 11:31:51 -04:00
Pete Markowsky
160195a1d4 Implement NSSecureCoding for SNTRuleIdentifiers (#1307)
* Fix an issue with santactl fileinfo by implementing NSSecureCoding for SNTRuleIdentifiers.
2024-03-11 10:03:49 -04:00
Matt W
f2ce92650b Add required dep for internal builds (#1302) 2024-03-05 15:39:59 -05:00
Matt W
e89cdbcf64 Add support for CDHash rule types (#1301)
* Support CDHash rules

* Ensure hardened runtime for cdhash eval. Update docs.

* minor fixups

* Clarify docs
2024-03-05 15:07:36 -05:00
Pete Markowsky
6a697e00ea Added clean flags for JSON rule import (#1300)
* Add --clean and --clean-all flags to the santactl rule command to allow clearing the rule database when importing rules via JSON.
2024-03-03 11:12:53 -05:00
Matt W
74d8fe30d1 Creating transitive rules for rename events should fallback to destination path (#1299)
* Transitive rules should fallback to destination for RENAME events

* Add tests to exercise fallback for rename events
2024-02-28 17:09:07 -05:00
Matt W
7513c75f88 Refactor rule and count lookups (#1298)
* Refactor rule and count lookups

* Remove commented out code

* Change rule count types to int64_t. SNTRuleIdentifiers properties now RO.
2024-02-26 15:09:51 -05:00
Matt W
9bee43130e Make FileChangesRegex apply to all file change event types (#1294)
* Make FileChangesRegex apply to all file change event types

* Handle older SDKs

* Formatting

* Remove debug log
2024-02-22 10:12:02 -05:00
Nick Gregory
7fa23d4b97 Some more lint fixes (#1295)
* lint fixes

* more lint
2024-02-20 15:39:24 -05:00
Nick Gregory
42eb0a3669 ProcessTree: add macOS specific loader and ES adapter (2/4) (#1237)
* ProcessTree: add macos-specific loader and event adapter

* lingering darwin->macos

* lint

* remove defunct client id

* struct rename

* and one last header update

* use EndpointSecurityAPI in adapter

* expose esapi in message
2024-02-20 13:56:54 -05:00
Russell Hancox
1ea26f0ac9 docs: Document that *PathRegex does not work on symlinks (#1290) 2024-02-13 18:53:17 -05:00
Nick Gregory
c35e9978d3 ProcessTree: fix missing direct deps (#1288)
* hmm

* more lint and add another dep
2024-02-09 10:33:57 -05:00
Matt W
e4c0d56bb6 Remove proc tree tests for now as the code isn't yet included in santa builds (#1287) 2024-02-08 16:01:47 -05:00
Matt W
908b1bcabe Add build dep for internal process (#1286) 2024-02-08 15:43:01 -05:00
Matt W
64e81bedc6 Respect fail closed on deadlines (#1285)
* Responses to events about to exceed deadline should respect FailClosed

* Only respect FailClosed when in Lockdown mode. Update docs.

* FailClosed in Configurator now wraps checking client mode

* PR feedback

* Fix execution controller tests with new FailClosed logic
2024-02-08 15:12:05 -05:00
Matt W
5dfab22fa7 Fix automatically denied events with small deadlines (#1284)
* Fix automatically denied events with small deadlines

* Fix up additional tests that had defined deadline interactions
2024-02-08 10:25:06 -05:00
Nick Gregory
5248e2a7eb Fix import issues and lint (#1282)
* lint

* case insensitive filesystems ahhhh

* tidy

* one last header
2024-02-07 17:46:42 -05:00
Nick Gregory
e8db89c57c ProcessTree: add core process tree logic (1/4) (#1236)
* ProcessTree: add core process tree logic

* make Step implicitly called by Handle* methods

* lint

* naming convention

* widen pidversion to be generic

* move os specific backfill to os specific impl

* simplify ts checking

* retain/release a whole vec of pids

* document processtoken

* lint

* namespace

* add process tree to project-wide unit test target

* case change annotations

* case change annotations

* remove stray comment

* default initialize seen_timestamps

* fix missing initialization of refcnt and tombstoned

* reshuffle pb namespace

* pr review

* move annotation registration to tree construction

* use factory function for tree construction
2024-02-05 14:30:54 -05:00
89 changed files with 3325 additions and 628 deletions

View File

@@ -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",

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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.
///

View File

@@ -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;
}

View File

@@ -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.
///

View File

@@ -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;

View File

@@ -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;
}

View 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

View 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

View File

@@ -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.
///

View File

@@ -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");

View File

@@ -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;

View File

@@ -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";

View File

@@ -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;
///

View File

@@ -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;
///

View File

@@ -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)) {
}
}
/**

View File

@@ -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;

View File

@@ -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",

View File

@@ -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.

View File

@@ -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",

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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];

View File

@@ -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 {

View File

@@ -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

View File

@@ -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) {

View File

@@ -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;
};

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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));

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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());

View File

@@ -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";

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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) {

View 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",
],
)

View 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

View 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

View 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",
],
)

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,6 @@
syntax = "proto3";
package santa.pb.v1.process_tree;
message Annotations {
}

View 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

View 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 executables 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

View 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

View 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

View 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

View File

@@ -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]);
}
}
}

View File

@@ -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];
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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]);
}

View File

@@ -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";

View File

@@ -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());

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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

View File

@@ -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) {

View File

@@ -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");

View File

@@ -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>

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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 |

View File

@@ -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.