Skip to content

Commit 1bfef5f

Browse files
authored
Add a backoff cache for unactionable stored events (#623)
1 parent 300fac4 commit 1bfef5f

File tree

12 files changed

+176
-8
lines changed

12 files changed

+176
-8
lines changed

Source/common/SNTStoredEvent.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,8 @@
2828
// The base class implementation will throw an exception.
2929
- (nullable NSString *)uniqueID;
3030

31+
// Subclasses are required to define: `unactionableEvent`.
32+
// The base class implementation will throw an exception.
33+
- (BOOL)unactionableEvent;
34+
3135
@end

Source/common/SNTStoredEvent.mm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,9 @@ - (NSString *)uniqueID {
6565
return nil;
6666
}
6767

68+
- (BOOL)unactionableEvent {
69+
[self doesNotRecognizeSelector:_cmd];
70+
return NO;
71+
}
72+
6873
@end

Source/common/SNTStoredEventTest.mm

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,26 @@ - (void)testUniqueID {
4242
XCTAssertEqualObjects([faaEvent uniqueID], @"MyRule|MyVersion|bar");
4343
}
4444

45+
- (void)testUnactionableEvent {
46+
// The base class should throw
47+
SNTStoredEvent *baseEvent = [[SNTStoredEvent alloc] init];
48+
XCTAssertThrows([baseEvent unactionableEvent]);
49+
50+
// Spot check allow/block events
51+
SNTStoredExecutionEvent *execEvent = [[SNTStoredExecutionEvent alloc] init];
52+
execEvent.decision = SNTEventStateAllowBinary;
53+
XCTAssertTrue([execEvent unactionableEvent]);
54+
execEvent.decision = SNTEventStateBlockBinary;
55+
XCTAssertFalse([execEvent unactionableEvent]);
56+
57+
// Spot check audit only/denied events
58+
SNTStoredFileAccessEvent *faaEvent = [[SNTStoredFileAccessEvent alloc] init];
59+
faaEvent.decision = FileAccessPolicyDecision::kAllowedAuditOnly;
60+
XCTAssertTrue([faaEvent unactionableEvent]);
61+
faaEvent.decision = FileAccessPolicyDecision::kDenied;
62+
XCTAssertFalse([faaEvent unactionableEvent]);
63+
}
64+
4565
- (void)testEncodeDecode {
4666
SNTStoredExecutionEvent *execEvent = [[SNTStoredExecutionEvent alloc] init];
4767
execEvent.fileSHA256 = @"foo";

Source/common/SNTStoredExecutionEvent.mm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,8 @@ - (NSString *)uniqueID {
171171
return self.fileSHA256;
172172
}
173173

174+
- (BOOL)unactionableEvent {
175+
return (self.decision & SNTEventStateAllow) != 0;
176+
}
177+
174178
@end

Source/common/SNTStoredFileAccessEvent.mm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ - (NSString *)uniqueID {
6262
return [NSString stringWithFormat:@"%@|%@|%@", _ruleName, _ruleVersion, _process.fileSHA256];
6363
}
6464

65+
- (BOOL)unactionableEvent {
66+
return self.decision == FileAccessPolicyDecision::kAllowedAuditOnly;
67+
}
68+
6569
@end
6670

6771
@implementation SNTStoredFileAccessProcess

Source/santad/BUILD

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ objc_library(
5757
"//Source/common:SNTLogging",
5858
"//Source/common:SNTStoredExecutionEvent",
5959
"//Source/common:SNTStoredFileAccessEvent",
60+
"//Source/common:SantaCache",
61+
"//Source/common:String",
6062
],
6163
)
6264

@@ -1061,6 +1063,9 @@ santa_unit_test(
10611063
"//Source/common:SNTLogging",
10621064
"//Source/common:SNTStoredExecutionEvent",
10631065
"//Source/common:SNTStoredFileAccessEvent",
1066+
"//Source/common:SantaCache",
1067+
"//Source/common:String",
1068+
"//Source/common:TestUtils",
10641069
"@FMDB",
10651070
"@OCMock",
10661071
],

Source/santad/DataLayer/SNTEventTable.mm

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,37 @@
1515

1616
#import "Source/santad/DataLayer/SNTEventTable.h"
1717

18+
#include <memory>
19+
1820
#import "Source/common/MOLCertificate.h"
1921
#import "Source/common/SNTLogging.h"
2022
#import "Source/common/SNTStoredEvent.h"
2123
#import "Source/common/SNTStoredExecutionEvent.h"
2224
#import "Source/common/SNTStoredFileAccessEvent.h"
25+
#include "Source/common/SantaCache.h"
26+
#include "Source/common/String.h"
2327

2428
static const uint32_t kEventTableCurrentVersion = 5;
29+
// 4 hour cache
30+
static const NSTimeInterval kUnactionableEventCacheTimeSeconds = (60 * 60 * 4);
31+
32+
@interface SNTEventTable ()
33+
// This property is only set once, safe to be nonatomic
34+
@property(nonatomic) NSTimeInterval unactionableEventCacheTimeSeconds;
35+
@end
2536

26-
@implementation SNTEventTable
37+
@implementation SNTEventTable {
38+
std::unique_ptr<SantaCache<std::string, NSDate *>> _storeBackoff;
39+
}
2740

2841
- (uint32_t)currentSupportedVersion {
2942
return kEventTableCurrentVersion;
3043
}
3144

3245
- (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version {
46+
_storeBackoff = std::make_unique<SantaCache<std::string, NSDate *>>(2048);
47+
self.unactionableEventCacheTimeSeconds = kUnactionableEventCacheTimeSeconds;
48+
3349
int newVersion = 0;
3450

3551
if (version < 1) {
@@ -112,6 +128,10 @@ - (BOOL)addStoredEvents:(NSArray<SNTStoredEvent *> *)events {
112128
continue;
113129
}
114130

131+
if ([event unactionableEvent] && [self backoffForPrimaryHash:[event uniqueID]]) {
132+
continue;
133+
}
134+
115135
NSData *eventData = [NSKeyedArchiver archivedDataWithRootObject:event
116136
requiringSecureCoding:YES
117137
error:nil];
@@ -135,6 +155,18 @@ - (BOOL)addStoredEvents:(NSArray<SNTStoredEvent *> *)events {
135155
return success;
136156
}
137157

158+
- (BOOL)backoffForPrimaryHash:(NSString *)hash {
159+
NSDate *backoff = _storeBackoff->get(santa::NSStringToUTF8String(hash));
160+
NSDate *now = [NSDate date];
161+
if (([now timeIntervalSince1970] - [backoff timeIntervalSince1970]) <
162+
self.unactionableEventCacheTimeSeconds) {
163+
return YES;
164+
} else {
165+
_storeBackoff->set(santa::NSStringToUTF8String(hash), now);
166+
return NO;
167+
}
168+
}
169+
138170
#pragma mark Querying/Retreiving
139171

140172
- (NSUInteger)pendingEventsCount {

Source/santad/DataLayer/SNTEventTableTest.mm

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#import "Source/common/SNTFileInfo.h"
2525
#import "Source/common/SNTStoredExecutionEvent.h"
2626
#import "Source/common/SNTStoredFileAccessEvent.h"
27+
#include "Source/common/TestUtils.h"
2728

2829
NSString *GenerateRandomHexStringWithSHA256Length() {
2930
// Create an array to hold random bytes
@@ -48,6 +49,7 @@
4849

4950
@interface SNTEventTable (Testing)
5051
- (SNTStoredEvent *)eventFromResultSet:(FMResultSet *)rs;
52+
@property NSTimeInterval unactionableEventCacheTimeSeconds;
5153
@end
5254

5355
/// This test case actually tests SNTEventTable and SNTStoredEvent.
@@ -82,7 +84,7 @@ - (SNTStoredExecutionEvent *)createTestEvent {
8284
event.executingUser = @"nobody";
8385
event.loggedInUsers = @[ @"nobody" ];
8486
event.currentSessions = @[ @"nobody@ttys000", @"nobody@console" ];
85-
event.decision = SNTEventStateAllowBinary;
87+
event.decision = SNTEventStateBlockBinary;
8688
return event;
8789
}
8890

@@ -100,6 +102,7 @@ - (SNTStoredFileAccessEvent *)createTestFileAccessEvent {
100102
event.process.pid = @(1234);
101103
event.process.parent = [[SNTStoredFileAccessProcess alloc] init];
102104
event.process.parent.pid = @(4567);
105+
event.decision = FileAccessPolicyDecision::kDenied;
103106
return event;
104107
}
105108

@@ -111,7 +114,7 @@ - (void)testAddEvent {
111114
XCTAssertEqual(self.sut.pendingEventsCount, 2);
112115
}
113116

114-
- (void)testUniqueIndex {
117+
- (void)testUniqueIndexActionable {
115118
XCTAssertEqual(self.sut.pendingEventsCount, 0);
116119

117120
SNTStoredExecutionEvent *event = [self createTestEvent];
@@ -157,6 +160,94 @@ - (void)testUniqueIndex {
157160
XCTAssertEqual(self.sut.pendingEventsCount, 4);
158161
}
159162

163+
- (void)testUniqueIndexUnactionable {
164+
XCTAssertEqual(self.sut.pendingEventsCount, 0);
165+
166+
SNTStoredExecutionEvent *event = [self createTestEvent];
167+
// Make this an "unactionable" event
168+
event.decision = SNTEventStateAllowBinary;
169+
XCTAssertTrue([self.sut addStoredEvent:event]);
170+
XCTAssertEqual(self.sut.pendingEventsCount, 1);
171+
172+
// Attempt to add an event with the same file hash will fail because
173+
// no insert will be attempted
174+
event.idx = @(arc4random());
175+
XCTAssertFalse([self.sut addStoredEvent:event]);
176+
XCTAssertEqual(self.sut.pendingEventsCount, 1);
177+
178+
// Create a new hash and re-insert
179+
event.idx = @(arc4random());
180+
event.fileSHA256 = GenerateRandomHexStringWithSHA256Length();
181+
XCTAssertTrue([self.sut addStoredEvent:event]);
182+
XCTAssertEqual(self.sut.pendingEventsCount, 2);
183+
184+
// Attempting to add an event with a non-unique idx fails
185+
event.fileSHA256 = GenerateRandomHexStringWithSHA256Length();
186+
XCTAssertFalse([self.sut addStoredEvent:event]);
187+
XCTAssertEqual(self.sut.pendingEventsCount, 2);
188+
189+
// Now for FAA Events...
190+
SNTStoredFileAccessEvent *faaEvent = [self createTestFileAccessEvent];
191+
// Make this an "unactionable" event
192+
faaEvent.decision = FileAccessPolicyDecision::kAllowedAuditOnly;
193+
XCTAssertTrue([self.sut addStoredEvent:faaEvent]);
194+
XCTAssertEqual(self.sut.pendingEventsCount, 3);
195+
196+
// Attempt to add an event with the same file hash will fail because
197+
// no insert will be attempted
198+
faaEvent.idx = @(arc4random());
199+
XCTAssertFalse([self.sut addStoredEvent:faaEvent]);
200+
XCTAssertEqual(self.sut.pendingEventsCount, 3);
201+
202+
// Create a new hash and re-insert
203+
faaEvent.process.fileSHA256 = GenerateRandomHexStringWithSHA256Length();
204+
XCTAssertTrue([self.sut addStoredEvent:faaEvent]);
205+
XCTAssertEqual(self.sut.pendingEventsCount, 4);
206+
207+
// Attempting to add an event with a non-unique idx fails
208+
faaEvent.process.fileSHA256 = GenerateRandomHexStringWithSHA256Length();
209+
XCTAssertFalse([self.sut addStoredEvent:faaEvent]);
210+
XCTAssertEqual(self.sut.pendingEventsCount, 4);
211+
}
212+
213+
- (void)testBackoff {
214+
XCTAssertEqual(self.sut.pendingEventsCount, 0);
215+
216+
// Set the backoff time to 3 seconds
217+
self.sut.unactionableEventCacheTimeSeconds = 3;
218+
219+
SNTStoredExecutionEvent *event = [self createTestEvent];
220+
// Make this an "unactionable" event
221+
event.decision = SNTEventStateAllowBinary;
222+
NSNumber *origId = event.idx;
223+
XCTAssertTrue([self.sut addStoredEvent:event]);
224+
XCTAssertEqual(self.sut.pendingEventsCount, 1);
225+
226+
// Attempt to add an event with the same file hash will fail because
227+
// no insert will be attempted
228+
event.idx = @(arc4random());
229+
XCTAssertFalse([self.sut addStoredEvent:event]);
230+
XCTAssertEqual(self.sut.pendingEventsCount, 1);
231+
232+
// Delete everything
233+
[self.sut deleteEventWithId:origId];
234+
XCTAssertEqual(self.sut.pendingEventsCount, 0);
235+
236+
// Attempt to reinsert an unactionable event before the backoff time
237+
// This should still fail, even though it wouldn't conflict in the DB
238+
event.idx = @(arc4random());
239+
XCTAssertFalse([self.sut addStoredEvent:event]);
240+
XCTAssertEqual(self.sut.pendingEventsCount, 0);
241+
242+
// Sleep for the backoff time
243+
SleepMS(3000);
244+
245+
// Now re-add the stored event, all should be well
246+
event.idx = @(arc4random());
247+
XCTAssertTrue([self.sut addStoredEvent:event]);
248+
XCTAssertEqual(self.sut.pendingEventsCount, 1);
249+
}
250+
160251
- (void)testRetrieveExecutionEvent {
161252
SNTStoredExecutionEvent *event = [self createTestEvent];
162253
[self.sut addStoredEvent:event];

Source/santad/SNTSyncdQueue.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323

2424
@interface SNTSyncdQueue : NSObject
2525

26+
- (instancetype)initWithCacheSize:(uint64_t)cacheSize;
27+
- (instancetype)init NS_UNAVAILABLE;
28+
2629
- (void)reassessSyncServiceConnection;
2730
- (void)reassessSyncServiceConnectionImmediately;
2831

Source/santad/SNTSyncdQueue.mm

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ @implementation SNTSyncdQueue {
3939
std::unique_ptr<SantaCache<std::string, NSDate *>> _uploadBackoff;
4040
}
4141

42-
- (instancetype)init {
42+
- (instancetype)initWithCacheSize:(uint64_t)cacheSize {
4343
self = [super init];
4444
if (self) {
45-
_uploadBackoff = std::make_unique<SantaCache<std::string, NSDate *>>(256);
45+
_uploadBackoff = std::make_unique<SantaCache<std::string, NSDate *>>(cacheSize);
4646
_syncdQueue = dispatch_queue_create("com.northpolesec.syncd_queue",
4747
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
4848
}

0 commit comments

Comments
 (0)