Skip to content

Commit 6ec8764

Browse files
authored
santasyncservice: parse, dispatch, and report network flow rules (#976)
Adds the validator scaffolding, sync-response parsing, and pre/postflight reporting: - SNDNetworkFlowRuleValidator interface + always-YES stub in the santanetd module (real impl lands later), following the SNDFilterConfigurationHelper pattern. - Parse network_flow_rules from RuleDownloadResponse into SNTNetworkFlowRule (validating + dropping invalid), and dispatch them via the existing combined databaseRuleAddExecutionRules:fileAccessRules:networkFlowRules:... XPC. - Populate network_flow_rule_count / network_flow_rules_hash in preflight and network_flow_rules_received/_processed/_hash in postflight, gated on syncv2.
1 parent b3821cb commit 6ec8764

13 files changed

Lines changed: 211 additions & 5 deletions

Source/common/SNTSyncConstants.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ extern NSString* const kTeamIDRuleCount;
4545
extern NSString* const kSigningIDRuleCount;
4646
extern NSString* const kCDHashRuleCount;
4747
extern NSString* const kFileAccessRuleCount;
48+
extern NSString* const kNetworkFlowRuleCount;
4849
extern NSString* const kFullSyncInterval;
4950
extern NSString* const kFCMToken;
5051
extern NSString* const kFCMFullSyncInterval;

Source/common/SNTSyncConstants.mm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
NSString* const kSigningIDRuleCount = @"signingid_rule_count";
4646
NSString* const kCDHashRuleCount = @"cdhash_rule_count";
4747
NSString* const kFileAccessRuleCount = @"file_access_rule_count";
48+
NSString* const kNetworkFlowRuleCount = @"network_flow_rule_count";
4849
NSString* const kFullSyncInterval = @"full_sync_interval";
4950
NSString* const kFCMToken = @"fcm_token";
5051
NSString* const kFCMFullSyncInterval = @"fcm_full_sync_interval";

Source/santasyncservice/BUILD

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,15 @@ objc_library(
169169
":SNTSyncState",
170170
"//Source/common:SNTCommonEnums",
171171
"//Source/common:SNTFileAccessRule",
172+
"//Source/common:SNTNetworkFlowRule",
172173
"//Source/common:SNTRule",
173174
"//Source/common:SNTSyncConstants",
174175
"//Source/common:SNTXPCControlInterface",
175176
"//Source/common:String",
176177
"//Source/common/faa:WatchItemPolicy",
177178
"//Source/common/faa:WatchItems",
178179
"@northpolesec_protos//syncv2:v2_cc_proto",
180+
"@santanetd//src/santanetd:SNDNetworkFlowRuleValidator",
179181
],
180182
)
181183

@@ -187,6 +189,7 @@ santa_unit_test(
187189
deps = [
188190
":SNTSyncRuleDownload",
189191
"//Source/common:SNTFileAccessRule",
192+
"//Source/common:SNTNetworkFlowRule",
190193
"//Source/common:TestUtils",
191194
"//Source/common/faa:WatchItemPolicy",
192195
"//Source/common/faa:WatchItems",
@@ -380,6 +383,7 @@ santa_unit_test(
380383
"//Source/common:SNTFileInfo",
381384
"//Source/common:SNTLogging",
382385
"//Source/common:SNTMetricSet",
386+
"//Source/common:SNTNetworkFlowRule",
383387
"//Source/common:SNTRule",
384388
"//Source/common:SNTSIPStatus",
385389
"//Source/common:SNTStoredEvent",
@@ -400,6 +404,7 @@ santa_unit_test(
400404
"@northpolesec_protos//sync:v1_cc_proto",
401405
"@northpolesec_protos//syncv2:v2_cc_proto",
402406
"@protobuf//src/google/protobuf/json",
407+
"@santanetd//src/santanetd:SNDNetworkFlowRuleValidator",
403408
],
404409
)
405410

Source/santasyncservice/SNTSyncPostflight.mm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ BOOL Postflight(SNTSyncPostflight* self) {
4040
static_cast<uint32_t>(self.syncState.fileAccessRulesReceived));
4141
req->set_file_access_rules_processed(
4242
static_cast<uint32_t>(self.syncState.fileAccessRulesProcessed));
43+
req->set_network_flow_rules_received(
44+
static_cast<uint32_t>(self.syncState.networkFlowRulesReceived));
45+
req->set_network_flow_rules_processed(
46+
static_cast<uint32_t>(self.syncState.networkFlowRulesProcessed));
4347
}
4448

4549
switch (self.syncState.syncType) {
@@ -53,6 +57,7 @@ BOOL Postflight(SNTSyncPostflight* self) {
5357
req->set_rules_hash(santa::NSStringToUTF8String(execRulesHash));
5458
if constexpr (IsV2) {
5559
req->set_file_access_rules_hash(santa::NSStringToUTF8String(faaRulesHash));
60+
req->set_network_flow_rules_hash(santa::NSStringToUTF8String(nfRulesHash));
5661
}
5762
}];
5863

Source/santasyncservice/SNTSyncPreflight.mm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,15 @@ BOOL Preflight(SNTSyncPreflight* self, google::protobuf::Arena* arena,
125125
req->set_cdhash_rule_count(static_cast<uint32_t>(counts.cdhash));
126126
if constexpr (IsV2) {
127127
req->set_file_access_rule_count(static_cast<uint32_t>(counts.fileAccess));
128+
req->set_network_flow_rule_count(static_cast<uint32_t>(counts.networkFlow));
128129
}
129130
}];
130131

131132
[rop databaseRulesHash:^(NSString* execRulesHash, NSString* faaRulesHash, NSString* nfRulesHash) {
132133
req->set_rules_hash(NSStringToUTF8String(execRulesHash));
133134
if constexpr (IsV2) {
134135
req->set_file_access_rules_hash(NSStringToUTF8String(faaRulesHash));
136+
req->set_network_flow_rules_hash(NSStringToUTF8String(nfRulesHash));
135137
}
136138
}];
137139

Source/santasyncservice/SNTSyncRuleDownload.mm

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
#import "Source/common/MOLXPCConnection.h"
2222
#import "Source/common/SNTFileAccessRule.h"
23+
#import "Source/common/SNTNetworkFlowRule.h"
2324
#import "Source/common/SNTRule.h"
2425
#import "Source/common/SNTSyncConstants.h"
2526
#import "Source/common/SNTXPCControlInterface.h"
@@ -32,6 +33,7 @@
3233
#import "Source/santasyncservice/SNTSyncLogging.h"
3334
#import "Source/santasyncservice/SNTSyncState.h"
3435
#include "google/protobuf/arena.h"
36+
#import "src/santanetd/SNDNetworkFlowRuleValidator.h"
3537
#include "syncv2/v2.pb.h"
3638

3739
namespace pbv2 = ::santa::sync::v2;
@@ -56,20 +58,24 @@ void ProcessDeprecatedBundleNotificationsForRule(
5658
NSDictionary* OptionsFromProtoFAARuleAdd(const ::pbv2::FileAccessRule::Add& pbAddRule);
5759
NSArray* ProcessesFromProtoFAARuleProcesses(
5860
const google::protobuf::RepeatedPtrField<::pbv2::FileAccessRule::Process>& pbProcesses);
61+
SNTNetworkFlowRule* NetworkFlowRuleFromProto(const ::pbv2::NetworkFlowRule& nr);
5962

6063
// Small local object to more easily return the different sets of downloaded rules.
6164
@interface SNTDownloadedRuleSets : NSObject
6265
@property(readonly) NSArray<SNTRule*>* executionRules;
6366
@property(readonly) NSArray<SNTFileAccessRule*>* fileAccessRules;
67+
@property(readonly) NSArray<SNTNetworkFlowRule*>* networkRules;
6468
@end
6569

6670
@implementation SNTDownloadedRuleSets
6771
- (instancetype)initWithExecutionRules:(NSArray<SNTRule*>*)executionRules
68-
fileAccessRules:(NSArray<SNTFileAccessRule*>*)fileAccessRules {
72+
fileAccessRules:(NSArray<SNTFileAccessRule*>*)fileAccessRules
73+
networkRules:(NSArray<SNTNetworkFlowRule*>*)networkRules {
6974
self = [super init];
7075
if (self) {
7176
_executionRules = executionRules;
7277
_fileAccessRules = fileAccessRules;
78+
_networkRules = networkRules;
7379
}
7480
return self;
7581
}
@@ -94,8 +100,10 @@ SNTRuleCleanup SyncTypeToRuleCleanup(SNTSyncType syncType) {
94100

95101
self.syncState.rulesReceived = 0;
96102
self.syncState.fileAccessRulesReceived = 0;
103+
self.syncState.networkFlowRulesReceived = 0;
97104
NSMutableArray<SNTRule*>* newRules = [NSMutableArray array];
98105
NSMutableArray<SNTFileAccessRule*>* newFileAccessRules = [NSMutableArray array];
106+
NSMutableArray<SNTNetworkFlowRule*>* newNetworkRules = [NSMutableArray array];
99107
std::string cursor;
100108

101109
do {
@@ -135,22 +143,34 @@ SNTRuleCleanup SyncTypeToRuleCleanup(SNTSyncType syncType) {
135143
}
136144
[newFileAccessRules addObject:rule];
137145
}
146+
147+
for (const ::pbv2::NetworkFlowRule& networkRule : response.network_flow_rules()) {
148+
SNTNetworkFlowRule* rule = NetworkFlowRuleFromProto(networkRule);
149+
if (!rule) {
150+
SLOGD(@"Ignoring bad network flow rule: %s", networkRule.Utf8DebugString().c_str());
151+
continue;
152+
}
153+
[newNetworkRules addObject:rule];
154+
}
138155
}
139156

140157
cursor = response.cursor();
141158
SLOGI(@"Received %lu rules", (unsigned long)response.rules_size());
142159
self.syncState.rulesReceived += response.rules_size();
143160
if constexpr (IsV2) {
144161
self.syncState.fileAccessRulesReceived += response.file_access_rules_size();
162+
self.syncState.networkFlowRulesReceived += response.network_flow_rules_size();
145163
}
146164
}
147165
} while (!cursor.empty());
148166

149167
self.syncState.rulesProcessed = newRules.count;
150168
self.syncState.fileAccessRulesProcessed = newFileAccessRules.count;
169+
self.syncState.networkFlowRulesProcessed = newNetworkRules.count;
151170

152171
return [[SNTDownloadedRuleSets alloc] initWithExecutionRules:newRules
153-
fileAccessRules:newFileAccessRules];
172+
fileAccessRules:newFileAccessRules
173+
networkRules:newNetworkRules];
154174
}
155175

156176
NSArray* PathsFromProtoFAARulePaths(
@@ -299,6 +319,27 @@ SNTRuleCleanup SyncTypeToRuleCleanup(SNTSyncType syncType) {
299319
}
300320
}
301321

322+
SNTNetworkFlowRule* NetworkFlowRuleFromProto(const ::pbv2::NetworkFlowRule& nr) {
323+
switch (nr.action_case()) {
324+
case ::pbv2::NetworkFlowRule::kAdd: {
325+
std::string serialized;
326+
nr.add().SerializeToString(&serialized);
327+
NSData* blob = [NSData dataWithBytes:serialized.data() length:serialized.size()];
328+
329+
NSError* err;
330+
if (!SNDValidateNetworkFlowRule(blob, &err)) {
331+
SLOGW(@"Dropping invalid network flow rule %lld: %@", (long long)nr.add().rule_id(),
332+
err.localizedDescription ?: @"validation failed");
333+
return nil;
334+
}
335+
return [[SNTNetworkFlowRule alloc] initAddRuleWithId:nr.add().rule_id() protoBlob:blob];
336+
}
337+
case ::pbv2::NetworkFlowRule::kRemove:
338+
return [[SNTNetworkFlowRule alloc] initRemoveRuleWithId:nr.remove().rule_id()];
339+
default: return nil;
340+
}
341+
}
342+
302343
template <bool IsV2>
303344
SNTRule* RuleFromProtoRule(const typename santa::ProtoTraits<IsV2>::RuleT& rule) {
304345
using Traits = santa::ProtoTraits<IsV2>;
@@ -435,7 +476,8 @@ - (BOOL)sync {
435476
return NO;
436477
}
437478
// If the request was successfully completed, but no new rules received, just return
438-
if (!newRules.executionRules.count && !newRules.fileAccessRules.count) {
479+
if (!newRules.executionRules.count && !newRules.fileAccessRules.count &&
480+
!newRules.networkRules.count) {
439481
return YES;
440482
}
441483

@@ -447,7 +489,7 @@ - (BOOL)sync {
447489
[[self.daemonConn remoteObjectProxy]
448490
databaseRuleAddExecutionRules:newRules.executionRules
449491
fileAccessRules:newRules.fileAccessRules
450-
networkFlowRules:nil
492+
networkFlowRules:newRules.networkRules
451493
ruleCleanup:SyncTypeToRuleCleanup(self.syncState.syncType)
452494
source:SNTRuleAddSourceSyncService
453495
reply:^(BOOL didSucceed, NSArray<NSError*>* e) {
@@ -489,6 +531,10 @@ - (BOOL)sync {
489531
SLOGI(@"Processed %lu file access rules", newRules.fileAccessRules.count);
490532
}
491533

534+
if (newRules.networkRules.count) {
535+
SLOGI(@"Processed %lu network flow rules", newRules.networkRules.count);
536+
}
537+
492538
// Send out push notifications about any newly allowed binaries
493539
// that had been previously blocked by santad.
494540
[self announceUnblockingRules:newRules.executionRules];

Source/santasyncservice/SNTSyncRuleDownloadTest.mm

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#import <XCTest/XCTest.h>
1919

2020
#import "Source/common/SNTFileAccessRule.h"
21+
#import "Source/common/SNTNetworkFlowRule.h"
2122
#include "Source/common/TestUtils.h"
2223
#include "Source/common/faa/WatchItemPolicy.h"
2324
#include "Source/common/faa/WatchItems.h"
@@ -32,6 +33,7 @@
3233
extern NSArray* ProcessesFromProtoFAARuleProcesses(
3334
const google::protobuf::RepeatedPtrField<::pbv2::FileAccessRule::Process>& pbProcesses);
3435
extern SNTFileAccessRule* FAARuleFromProtoFileAccessRule(const ::pbv2::FileAccessRule& wi);
36+
extern SNTNetworkFlowRule* NetworkFlowRuleFromProto(const ::pbv2::NetworkFlowRule& nr);
3537

3638
@interface SNTSyncRuleDownloadTest : XCTestCase
3739
@end
@@ -330,6 +332,42 @@ - (void)testInvalidFAARuleFromProtoFileAccessRuleAdd {
330332
}
331333
}
332334

335+
- (void)testNetworkFlowRuleFromProtoAdd {
336+
::pbv2::NetworkFlowRule nr;
337+
::pbv2::NetworkFlowRule::Add* add = nr.mutable_add();
338+
add->set_rule_id(42);
339+
add->set_action(::pbv2::NetworkFlowRule::ACTION_DENY);
340+
add->set_direction(::pbv2::NETWORK_FLOW_DIRECTION_OUTGOING);
341+
342+
SNTNetworkFlowRule* rule = NetworkFlowRuleFromProto(nr);
343+
XCTAssertNotNil(rule);
344+
XCTAssertEqual(rule.ruleId, 42);
345+
XCTAssertEqual(rule.state, SNTNetworkFlowRuleStateAdd);
346+
XCTAssertNotNil(rule.protoBlob);
347+
348+
// The blob round-trips back to the originating Add.
349+
::pbv2::NetworkFlowRule::Add parsed;
350+
XCTAssertTrue(parsed.ParseFromArray(rule.protoBlob.bytes, (int)rule.protoBlob.length));
351+
XCTAssertEqual(parsed.rule_id(), 42);
352+
XCTAssertEqual(parsed.action(), ::pbv2::NetworkFlowRule::ACTION_DENY);
353+
}
354+
355+
- (void)testNetworkFlowRuleFromProtoRemove {
356+
::pbv2::NetworkFlowRule nr;
357+
nr.mutable_remove()->set_rule_id(99);
358+
359+
SNTNetworkFlowRule* rule = NetworkFlowRuleFromProto(nr);
360+
XCTAssertNotNil(rule);
361+
XCTAssertEqual(rule.ruleId, 99);
362+
XCTAssertEqual(rule.state, SNTNetworkFlowRuleStateRemove);
363+
XCTAssertNil(rule.protoBlob);
364+
}
365+
366+
- (void)testNetworkFlowRuleFromProtoEmptyIsNil {
367+
::pbv2::NetworkFlowRule nr;
368+
XCTAssertNil(NetworkFlowRuleFromProto(nr));
369+
}
370+
333371
- (void)testFAARuleFromProtoFileAccessRuleRemove {
334372
::pbv2::FileAccessRule wi;
335373
::pbv2::FileAccessRule::Remove* pbRemove = wi.mutable_remove();

Source/santasyncservice/SNTSyncState.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@
115115
@property NSUInteger rulesProcessed;
116116
@property NSUInteger fileAccessRulesReceived;
117117
@property NSUInteger fileAccessRulesProcessed;
118+
@property NSUInteger networkFlowRulesReceived;
119+
@property NSUInteger networkFlowRulesProcessed;
118120

119121
@property BOOL preflightOnly;
120122
@property BOOL pushNotificationSync;

Source/santasyncservice/SNTSyncTest.mm

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,7 @@ - (void)testPreflightDatabaseCounts {
530530
.signingID = 123,
531531
.cdhash = 11,
532532
.fileAccess = 513,
533+
.networkFlow = 77,
533534
};
534535

535536
OCMStub([self.daemonConnRop
@@ -549,8 +550,33 @@ - (void)testPreflightDatabaseCounts {
549550
XCTAssertEqualObjects(requestDict[kSigningIDRuleCount], @(ruleCounts.signingID));
550551
if (self.syncState.isSyncV2) {
551552
XCTAssertEqualObjects(requestDict[kFileAccessRuleCount], @(ruleCounts.fileAccess));
553+
XCTAssertEqualObjects(requestDict[kNetworkFlowRuleCount], @(ruleCounts.networkFlow));
552554
} else {
553555
XCTAssertNil(requestDict[kFileAccessRuleCount]);
556+
XCTAssertNil(requestDict[kNetworkFlowRuleCount]);
557+
}
558+
return YES;
559+
}];
560+
561+
[sut sync];
562+
}
563+
564+
- (void)testPreflightRulesHash {
565+
[self setupDefaultDaemonConnResponses];
566+
SNTSyncPreflight* sut = [[SNTSyncPreflight alloc] initWithState:self.syncState];
567+
568+
[self stubRequestBody:nil
569+
response:nil
570+
error:nil
571+
validateBlock:^BOOL(NSURLRequest* req) {
572+
NSDictionary* requestDict = [self dictFromRequest:req];
573+
XCTAssertEqualObjects(requestDict[@"rulesHash"], @"the-hash");
574+
if (self.syncState.isSyncV2) {
575+
XCTAssertEqualObjects(requestDict[@"fileAccessRulesHash"], @"the-faa-hash");
576+
XCTAssertEqualObjects(requestDict[@"network_flow_rules_hash"], @"the-nf-hash");
577+
} else {
578+
XCTAssertNil(requestDict[@"fileAccessRulesHash"]);
579+
XCTAssertNil(requestDict[@"network_flow_rules_hash"]);
554580
}
555581
return YES;
556582
}];
@@ -1207,6 +1233,7 @@ - (void)testPostflightBasicResponse {
12071233
XCTAssertEqualObjects(requestDict[@"rulesHash"], @"the-hash");
12081234
if (self.syncState.isSyncV2) {
12091235
XCTAssertEqualObjects(requestDict[@"fileAccessRulesHash"], @"the-faa-hash");
1236+
XCTAssertEqualObjects(requestDict[@"network_flow_rules_hash"], @"the-nf-hash");
12101237
}
12111238
return YES;
12121239
}];
@@ -1215,6 +1242,38 @@ - (void)testPostflightBasicResponse {
12151242
OCMVerify([self.daemonConnRop updateSyncSettings:OCMOCK_ANY reply:OCMOCK_ANY]);
12161243
}
12171244

1245+
- (void)testPostflightReportsRuleCounts {
1246+
[self setupDefaultDaemonConnResponses];
1247+
self.syncState.rulesReceived = 10;
1248+
self.syncState.rulesProcessed = 9;
1249+
self.syncState.fileAccessRulesReceived = 4;
1250+
self.syncState.fileAccessRulesProcessed = 3;
1251+
self.syncState.networkFlowRulesReceived = 6;
1252+
self.syncState.networkFlowRulesProcessed = 5;
1253+
SNTSyncPostflight* sut = [[SNTSyncPostflight alloc] initWithState:self.syncState];
1254+
1255+
[self stubRequestBody:nil
1256+
response:nil
1257+
error:nil
1258+
validateBlock:^BOOL(NSURLRequest* req) {
1259+
NSDictionary* requestDict = [self dictFromRequest:req];
1260+
XCTAssertEqualObjects(requestDict[@"rules_received"], @10);
1261+
XCTAssertEqualObjects(requestDict[@"rules_processed"], @9);
1262+
if (self.syncState.isSyncV2) {
1263+
XCTAssertEqualObjects(requestDict[@"file_access_rules_received"], @4);
1264+
XCTAssertEqualObjects(requestDict[@"file_access_rules_processed"], @3);
1265+
XCTAssertEqualObjects(requestDict[@"network_flow_rules_received"], @6);
1266+
XCTAssertEqualObjects(requestDict[@"network_flow_rules_processed"], @5);
1267+
} else {
1268+
XCTAssertNil(requestDict[@"network_flow_rules_received"]);
1269+
XCTAssertNil(requestDict[@"network_flow_rules_processed"]);
1270+
}
1271+
return YES;
1272+
}];
1273+
1274+
XCTAssertTrue([sut sync]);
1275+
}
1276+
12181277
- (SNTConfigBundle*)runPostflightAndCaptureBundleWithInflightSyncType:(SNTSyncType)inflight {
12191278
[self setupDefaultDaemonConnResponses];
12201279
self.syncState.syncType = inflight;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"serial_num":"QYGF4QM373","hostname":"full-hostname.example.com","os_version":"14.5","os_build":"23F79","model_identifier":"MacBookPro18,3","santa_version":"2024.6.655965194","primary_user":"username1","client_mode":"MONITOR","machine_id":"50C7E1EB-2EF5-42D4-A084-A7966FC45A95","sipStatus":111,"rulesHash":"the-hash","fileAccessRulesHash":"the-faa-hash"}
1+
{"serial_num":"QYGF4QM373","hostname":"full-hostname.example.com","os_version":"14.5","os_build":"23F79","model_identifier":"MacBookPro18,3","santa_version":"2024.6.655965194","primary_user":"username1","client_mode":"MONITOR","machine_id":"50C7E1EB-2EF5-42D4-A084-A7966FC45A95","sipStatus":111,"rulesHash":"the-hash","fileAccessRulesHash":"the-faa-hash","network_flow_rules_hash":"the-nf-hash"}

0 commit comments

Comments
 (0)