From 3af69d27d4f9bce6694b6fa3713b7cd3c8fde68d Mon Sep 17 00:00:00 2001 From: Matt White <436037+mlw@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:16:25 -0500 Subject: [PATCH 1/2] Add support for logging entitlements in EXEC events --- Source/common/BUILD | 6 ++ Source/common/SNTCachedDecision.h | 1 + Source/common/SNTDeepCopy.h | 27 ++++++ Source/common/SNTDeepCopy.m | 53 +++++++++++ Source/common/TestUtils.mm | 6 +- Source/common/santa.proto | 10 +++ Source/santad/BUILD | 1 + .../EndpointSecurity/Serializers/Protobuf.mm | 87 +++++++++++++++++++ .../Serializers/ProtobufTest.mm | 74 ++++++++++++---- Source/santad/SNTPolicyProcessor.m | 4 + Source/santad/testdata/protobuf/v1/exec.json | 40 ++++++++- Source/santad/testdata/protobuf/v2/exec.json | 40 ++++++++- Source/santad/testdata/protobuf/v4/exec.json | 40 ++++++++- Source/santad/testdata/protobuf/v5/exec.json | 40 ++++++++- Source/santad/testdata/protobuf/v6/exec.json | 40 ++++++++- 15 files changed, 445 insertions(+), 24 deletions(-) create mode 100644 Source/common/SNTDeepCopy.h create mode 100644 Source/common/SNTDeepCopy.m diff --git a/Source/common/BUILD b/Source/common/BUILD index f328ce2dd..d741e502c 100644 --- a/Source/common/BUILD +++ b/Source/common/BUILD @@ -40,6 +40,12 @@ objc_library( ], ) +objc_library( + name = "SNTDeepCopy", + srcs = ["SNTDeepCopy.m"], + hdrs = ["SNTDeepCopy.h"], +) + cc_library( name = "SantaCache", hdrs = ["SantaCache.h"], diff --git a/Source/common/SNTCachedDecision.h b/Source/common/SNTCachedDecision.h index 0e8d6cfb7..4ca4752b1 100644 --- a/Source/common/SNTCachedDecision.h +++ b/Source/common/SNTCachedDecision.h @@ -38,6 +38,7 @@ @property NSArray *certChain; @property NSString *teamID; @property NSString *signingID; +@property NSDictionary *entitlements; @property NSString *quarantineURL; diff --git a/Source/common/SNTDeepCopy.h b/Source/common/SNTDeepCopy.h new file mode 100644 index 000000000..6612a26f0 --- /dev/null +++ b/Source/common/SNTDeepCopy.h @@ -0,0 +1,27 @@ +/// 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 + +@interface NSArray (SNTDeepCopy) + +- (instancetype)sntDeepCopy; + +@end + +@interface NSDictionary (SNTDeepCopy) + +- (instancetype)sntDeepCopy; + +@end diff --git a/Source/common/SNTDeepCopy.m b/Source/common/SNTDeepCopy.m new file mode 100644 index 000000000..16bcc34f3 --- /dev/null +++ b/Source/common/SNTDeepCopy.m @@ -0,0 +1,53 @@ +/// 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 "Source/common/SNTDeepCopy.h" + +@implementation NSArray (SNTDeepCopy) + +- (instancetype)sntDeepCopy { + NSMutableArray<__kindof NSObject *> *deepCopy = [NSMutableArray arrayWithCapacity:self.count]; + for (id object in self) { + if ([object respondsToSelector:@selector(sntDeepCopy)]) { + [deepCopy addObject:[object sntDeepCopy]]; + } else if ([object respondsToSelector:@selector(copyWithZone:)]) { + [deepCopy addObject:[object copy]]; + } else { + [deepCopy addObject:object]; + } + } + return deepCopy; +} + +@end + +@implementation NSDictionary (SNTDeepCopy) + +- (instancetype)sntDeepCopy { + NSMutableDictionary<__kindof NSObject *, __kindof NSObject *> *deepCopy = + [NSMutableDictionary dictionary]; + for (id key in self) { + id value = self[key]; + if ([value respondsToSelector:@selector(sntDeepCopy)]) { + deepCopy[key] = [value sntDeepCopy]; + } else if ([value respondsToSelector:@selector(copyWithZone:)]) { + deepCopy[key] = [value copy]; + } else { + deepCopy[key] = value; + } + } + return deepCopy; +} + +@end diff --git a/Source/common/TestUtils.mm b/Source/common/TestUtils.mm index b52c1b94e..55bb7ddd9 100644 --- a/Source/common/TestUtils.mm +++ b/Source/common/TestUtils.mm @@ -92,7 +92,11 @@ es_process_t MakeESProcess(es_file_t *file, audit_token_t tok, audit_token_t par } uint32_t MaxSupportedESMessageVersionForCurrentOS() { - // Note: ES message v3 was only in betas. + // Notes: + // 1. ES message v3 was only in betas. + // 2. Message version 7 appeared in macOS 13.3, but features from that are + // not currently used. Leaving off support here so as to not require + // adding v7 test JSON files. if (@available(macOS 13.0, *)) { return 6; } else if (@available(macOS 12.3, *)) { diff --git a/Source/common/santa.proto b/Source/common/santa.proto index fed5643b8..e09c6df72 100644 --- a/Source/common/santa.proto +++ b/Source/common/santa.proto @@ -213,6 +213,11 @@ message CertificateInfo { optional string common_name = 2; } +message Entitlement { + string key = 1; + string value = 2; +} + // Information about a process execution event message Execution { // The process that executed the new image (e.g. the process that called @@ -286,6 +291,11 @@ message Execution { // The original path on disk of the target executable // Applies when executables are translocated optional string original_path = 15; + + // The set of entitlements associated with the target executable + // Only top level keys are represented + // Values (including nested keys) are JSON serialized + repeated Entitlement entitlements = 16; } // Information about a fork event diff --git a/Source/santad/BUILD b/Source/santad/BUILD index d61b09b15..423c777bd 100644 --- a/Source/santad/BUILD +++ b/Source/santad/BUILD @@ -199,6 +199,7 @@ objc_library( "//Source/common:SNTCachedDecision", "//Source/common:SNTCommonEnums", "//Source/common:SNTConfigurator", + "//Source/common:SNTDeepCopy", "//Source/common:SNTFileInfo", "//Source/common:SNTLogging", "//Source/common:SNTRule", diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm b/Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm index 7c48029d7..0d0808c8b 100644 --- a/Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm +++ b/Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm @@ -71,6 +71,8 @@ namespace santa::santad::logs::endpoint_security::serializers { +static constexpr NSUInteger kMaxEncodeObjectEntries = 64; + std::shared_ptr Protobuf::Create(std::shared_ptr esapi, SNTDecisionCache *decision_cache, bool json) { return std::make_shared(esapi, std::move(decision_cache), json); @@ -449,6 +451,89 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info, return FinalizeProto(santa_msg); } +void EncodeEntitlements(::pbv1::Execution *pb_exec, NSDictionary *entitlements) { + if (!entitlements) { + return; + } + + __block int numObjectsToEncode = (int)std::min(kMaxEncodeObjectEntries, entitlements.count); + + pb_exec->mutable_entitlements()->Reserve(numObjectsToEncode); + + [entitlements enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + if (numObjectsToEncode-- == 0) { + *stop = YES; + return; + } + + if (![key isKindOfClass:[NSString class]]) { + LOGW(@"Skipping entitlement key with unexpected key type: %@", key); + return; + } + + NSError *err; + NSData *jsonData; + @try { + id val = obj; + + // Fixup some types with data that can be better represented in JSON + if ([obj isKindOfClass:[NSDate class]]) { + // Example format output: "November 6, 2023 at 10:25:20 AM EST" + val = [NSDateFormatter localizedStringFromDate:obj + dateStyle:NSDateFormatterLongStyle + timeStyle:NSDateFormatterLongStyle]; + } else if ([obj isKindOfClass:[NSData class]]) { + val = [obj base64EncodedStringWithOptions:0]; + } + + jsonData = [NSJSONSerialization dataWithJSONObject:val + options:NSJSONWritingFragmentsAllowed + error:&err]; + } @catch (NSException *e) { + LOGW(@"Encountered entitlement that cannot directly convert to JSON: %@: %@", key, obj); + } + + if (!jsonData) { + // If the first attempt to serialize to JSON failed, get a string + // representation of the object via the `description` method and attempt + // to serialize that instead. Serialization can fail for a number of + // reasons, such as arrays including invalid types. + @try { + jsonData = [NSJSONSerialization dataWithJSONObject:[obj description] + options:NSJSONWritingFragmentsAllowed + error:&err]; + } @catch (NSException *e) { + LOGW(@"Unable to create fallback string: %@: %@", key, obj); + } + + if (!jsonData) { + @try { + // As a final fallback, simply serialize an error message so that the + // entitlement key is still logged. + jsonData = [NSJSONSerialization dataWithJSONObject:@"JSON Serialization Failed" + options:NSJSONWritingFragmentsAllowed + error:&err]; + } @catch (NSException *e) { + // This shouldn't be able to happen... + LOGW(@"Failed to serialize fallback error message"); + } + } + } + + // This shouldn't be possible given the fallback code above. But handle it + // just in case to prevent a crash. + if (!jsonData) { + LOGW(@"Failed to create valid JSON for entitlement: %@", key); + return; + } + + ::pbv1::Entitlement *pb_entitlement = pb_exec->add_entitlements(); + pb_entitlement->set_key(NSStringToUTF8StringView(key)); + pb_entitlement->set_value(NSStringToUTF8StringView( + [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding])); + }]; +} + std::vector Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCachedDecision *cd) { Arena arena; ::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg); @@ -525,6 +610,8 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info, NSString *orig_path = Utilities::OriginalPathForTranslocation(msg.es_msg().event.exec.target); EncodeString([pb_exec] { return pb_exec->mutable_original_path(); }, orig_path); + EncodeEntitlements(pb_exec, cd.entitlements); + return FinalizeProto(santa_msg); } diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm b/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm index c508e4ad4..afd9d78dd 100644 --- a/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm +++ b/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm @@ -60,6 +60,7 @@ namespace santa::santad::logs::endpoint_security::serializers { extern void EncodeExitStatus(::pbv1::Exit *pbExit, int exitStatus); +extern void EncodeEntitlements(::pbv1::Execution *pb_exec, NSDictionary *entitlements); extern ::pbv1::Execution::Decision GetDecisionEnum(SNTEventState event_state); extern ::pbv1::Execution::Reason GetReasonEnum(SNTEventState event_state); extern ::pbv1::Execution::Mode GetModeEnum(SNTClientMode mode); @@ -68,6 +69,7 @@ extern ::pbv1::FileAccess::PolicyDecision GetPolicyDecision(FileAccessPolicyDecision decision); } // namespace santa::santad::logs::endpoint_security::serializers +using santa::santad::logs::endpoint_security::serializers::EncodeEntitlements; using santa::santad::logs::endpoint_security::serializers::EncodeExitStatus; using santa::santad::logs::endpoint_security::serializers::GetAccessType; using santa::santad::logs::endpoint_security::serializers::GetDecisionEnum; @@ -166,28 +168,35 @@ bool CompareTime(const Timestamp ×tamp, struct timespec ts) { return json; } -NSDictionary *findDelta(NSDictionary *a, NSDictionary *b) { - NSMutableDictionary *delta = NSMutableDictionary.dictionary; +NSDictionary *FindDelta(NSDictionary *want, NSDictionary *got) { + NSMutableDictionary *delta = [NSMutableDictionary dictionary]; + delta[@"want"] = [NSMutableDictionary dictionary]; + delta[@"got"] = [NSMutableDictionary dictionary]; - // Find objects in a that don't exist or are different in b. - [a enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) { - id otherObj = b[key]; + // Find objects in `want` that don't exist or are different in `got`. + [want enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + id otherObj = got[key]; - if (![obj isEqual:otherObj]) { - delta[key] = obj; + if (!otherObj) { + delta[@"want"][key] = obj; + delta[@"got"][key] = @"Key missing"; + } else if (![obj isEqual:otherObj]) { + delta[@"want"][key] = obj; + delta[@"got"][key] = otherObj; } }]; - // Find objects in the other dictionary that don't exist in self - [b enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) { - id aObj = a[key]; + // Find objects in `got` that don't exist in `want` + [got enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + id aObj = want[key]; if (!aObj) { - delta[key] = obj; + delta[@"want"][key] = @"Key missing"; + delta[@"got"][key] = obj; } }]; - return delta; + return [delta[@"want"] count] > 0 ? delta : nil; } void SerializeAndCheck(es_event_type_t eventType, @@ -267,10 +276,7 @@ void SerializeAndCheck(es_event_type_t eventType, options:NSJSONReadingMutableContainers error:&jsonError]; XCTAssertNil(jsonError, @"failed to parse got data as JSON"); - - // XCTAssertEqualObjects([NSString stringWithUTF8String:gotData.c_str()], wantData); - NSDictionary *delta = findDelta(wantJSONDict, gotJSONDict); - XCTAssertEqualObjects(@{}, delta); + XCTAssertNil(FindDelta(wantJSONDict, gotJSONDict)); } XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); @@ -338,6 +344,22 @@ - (void)setUp { self.testCachedDecision.quarantineURL = @"google.com"; self.testCachedDecision.certSHA256 = @"5678_cert_hash"; self.testCachedDecision.decisionClientMode = SNTClientModeLockdown; + self.testCachedDecision.entitlements = @{ + @"key_with_str_val" : @"bar", + @"key_with_num_val" : @(1234), + @"key_with_date_val" : [NSDate dateWithTimeIntervalSince1970:1699376402], + @"key_with_data_val" : [@"Hello World" dataUsingEncoding:NSUTF8StringEncoding], + @"key_with_arr_val" : @[ @"v1", @"v2", @"v3" ], + @"key_with_arr_val_nested" : @[ @"v1", @"v2", @"v3", @[ @"nv1", @"nv2" ] ], + @"key_with_arr_val_multitype" : + @[ @"v1", @"v2", @"v3", @(123), [NSDate dateWithTimeIntervalSince1970:1699376402] ], + @"key_with_dict_val" : @{@"k1" : @"v1", @"k2" : @"v2"}, + @"key_with_dict_val_nested" : @{ + @"k1" : @"v1", + @"k2" : @"v2", + @"k3" : @{@"nk1" : @"nv1", @"nk2" : [NSDate dateWithTimeIntervalSince1970:1699376402]} + }, + }; self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]); OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache); @@ -464,8 +486,8 @@ - (void)testGetFileDescriptorType { }; for (const auto &kv : fdtypeToEnumType) { - XCTAssertEqual(GetFileDescriptorType(kv.first), kv.second, @"Bad fd type name for fdtype: %u", - kv.first); + XCTAssertEqual(GetFileDescriptorType(kv.first), kv.second, + @"Bad fd type name for fdtype: %u", kv.first); } } @@ -573,6 +595,22 @@ - (void)testSerializeMessageExecJSON { json:YES]; } +- (void)testEncodeEntitlements { + ::pbv1::Execution pbExec; + NSMutableDictionary *ents = [NSMutableDictionary dictionary]; + + for (int i = 0; i < 100; i++) { + ents[[NSString stringWithFormat:@"k%d", i]] = @(i); + } + + XCTAssertEqual(0, pbExec.entitlements_size()); + + EncodeEntitlements(&pbExec, ents); + + int kMaxEncodeObjectEntries = 64; // From Protobuf.mm + XCTAssertEqual(kMaxEncodeObjectEntries, pbExec.entitlements_size()); +} + - (void)testSerializeMessageExit { [self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_EXIT messageSetup:^(std::shared_ptr mockESApi, diff --git a/Source/santad/SNTPolicyProcessor.m b/Source/santad/SNTPolicyProcessor.m index ffb17edb5..9c9b836a4 100644 --- a/Source/santad/SNTPolicyProcessor.m +++ b/Source/santad/SNTPolicyProcessor.m @@ -21,6 +21,7 @@ #import "Source/common/SNTCachedDecision.h" #import "Source/common/SNTConfigurator.h" +#import "Source/common/SNTDeepCopy.h" #import "Source/common/SNTFileInfo.h" #import "Source/common/SNTRule.h" #import "Source/santad/DataLayer/SNTRuleTable.h" @@ -92,6 +93,9 @@ - (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileIn cd.signingID = nil; } } + + cd.entitlements = + [csInfo.signingInformation[(__bridge NSString *)kSecCodeInfoEntitlementsDict] sntDeepCopy]; } } cd.quarantineURL = fileInfo.quarantineDataURL; diff --git a/Source/santad/testdata/protobuf/v1/exec.json b/Source/santad/testdata/protobuf/v1/exec.json index cfff6f661..bec33b2f9 100644 --- a/Source/santad/testdata/protobuf/v1/exec.json +++ b/Source/santad/testdata/protobuf/v1/exec.json @@ -120,5 +120,43 @@ } }, "explain": "extra!", - "quarantine_url": "google.com" + "quarantine_url": "google.com", + "entitlements": [ + { + "key": "key_with_arr_val", + "value": "[\"v1\",\"v2\",\"v3\"]" + }, + { + "key": "key_with_data_val", + "value": "\"SGVsbG8gV29ybGQ=\"" + }, + { + "key": "key_with_arr_val_multitype", + "value": "\"(\\n v1,\\n v2,\\n v3,\\n 123,\\n \\\"2023-11-07 17:00:02 +0000\\\"\\n)\"" + }, + { + "key": "key_with_arr_val_nested", + "value": "[\"v1\",\"v2\",\"v3\",[\"nv1\",\"nv2\"]]" + }, + { + "key": "key_with_dict_val", + "value": "{\"k1\":\"v1\",\"k2\":\"v2\"}" + }, + { + "key": "key_with_date_val", + "value": "\"November 7, 2023 at 5:00:02 PM GMT\"" + }, + { + "key": "key_with_dict_val_nested", + "value": "\"{\\n k1 = v1;\\n k2 = v2;\\n k3 = {\\n nk1 = nv1;\\n nk2 = \\\"2023-11-07 17:00:02 +0000\\\";\\n };\\n}\"" + }, + { + "key": "key_with_num_val", + "value": "1234" + }, + { + "key": "key_with_str_val", + "value": "\"bar\"" + } + ] } diff --git a/Source/santad/testdata/protobuf/v2/exec.json b/Source/santad/testdata/protobuf/v2/exec.json index 341f7a4f5..4340a488e 100644 --- a/Source/santad/testdata/protobuf/v2/exec.json +++ b/Source/santad/testdata/protobuf/v2/exec.json @@ -148,5 +148,43 @@ } }, "explain": "extra!", - "quarantine_url": "google.com" + "quarantine_url": "google.com", + "entitlements": [ + { + "key": "key_with_arr_val", + "value": "[\"v1\",\"v2\",\"v3\"]" + }, + { + "key": "key_with_data_val", + "value": "\"SGVsbG8gV29ybGQ=\"" + }, + { + "key": "key_with_arr_val_multitype", + "value": "\"(\\n v1,\\n v2,\\n v3,\\n 123,\\n \\\"2023-11-07 17:00:02 +0000\\\"\\n)\"" + }, + { + "key": "key_with_arr_val_nested", + "value": "[\"v1\",\"v2\",\"v3\",[\"nv1\",\"nv2\"]]" + }, + { + "key": "key_with_dict_val", + "value": "{\"k1\":\"v1\",\"k2\":\"v2\"}" + }, + { + "key": "key_with_date_val", + "value": "\"November 7, 2023 at 5:00:02 PM GMT\"" + }, + { + "key": "key_with_dict_val_nested", + "value": "\"{\\n k1 = v1;\\n k2 = v2;\\n k3 = {\\n nk1 = nv1;\\n nk2 = \\\"2023-11-07 17:00:02 +0000\\\";\\n };\\n}\"" + }, + { + "key": "key_with_num_val", + "value": "1234" + }, + { + "key": "key_with_str_val", + "value": "\"bar\"" + } + ] } diff --git a/Source/santad/testdata/protobuf/v4/exec.json b/Source/santad/testdata/protobuf/v4/exec.json index 455d9af41..c55d46e86 100644 --- a/Source/santad/testdata/protobuf/v4/exec.json +++ b/Source/santad/testdata/protobuf/v4/exec.json @@ -197,5 +197,43 @@ } }, "explain": "extra!", - "quarantine_url": "google.com" + "quarantine_url": "google.com", + "entitlements": [ + { + "key": "key_with_arr_val", + "value": "[\"v1\",\"v2\",\"v3\"]" + }, + { + "key": "key_with_data_val", + "value": "\"SGVsbG8gV29ybGQ=\"" + }, + { + "key": "key_with_arr_val_multitype", + "value": "\"(\\n v1,\\n v2,\\n v3,\\n 123,\\n \\\"2023-11-07 17:00:02 +0000\\\"\\n)\"" + }, + { + "key": "key_with_arr_val_nested", + "value": "[\"v1\",\"v2\",\"v3\",[\"nv1\",\"nv2\"]]" + }, + { + "key": "key_with_dict_val", + "value": "{\"k1\":\"v1\",\"k2\":\"v2\"}" + }, + { + "key": "key_with_date_val", + "value": "\"November 7, 2023 at 5:00:02 PM GMT\"" + }, + { + "key": "key_with_dict_val_nested", + "value": "\"{\\n k1 = v1;\\n k2 = v2;\\n k3 = {\\n nk1 = nv1;\\n nk2 = \\\"2023-11-07 17:00:02 +0000\\\";\\n };\\n}\"" + }, + { + "key": "key_with_num_val", + "value": "1234" + }, + { + "key": "key_with_str_val", + "value": "\"bar\"" + } + ] } diff --git a/Source/santad/testdata/protobuf/v5/exec.json b/Source/santad/testdata/protobuf/v5/exec.json index 83d10c542..b55e137ea 100644 --- a/Source/santad/testdata/protobuf/v5/exec.json +++ b/Source/santad/testdata/protobuf/v5/exec.json @@ -197,5 +197,43 @@ } }, "explain": "extra!", - "quarantine_url": "google.com" + "quarantine_url": "google.com", + "entitlements": [ + { + "key": "key_with_arr_val", + "value": "[\"v1\",\"v2\",\"v3\"]" + }, + { + "key": "key_with_data_val", + "value": "\"SGVsbG8gV29ybGQ=\"" + }, + { + "key": "key_with_arr_val_multitype", + "value": "\"(\\n v1,\\n v2,\\n v3,\\n 123,\\n \\\"2023-11-07 17:00:02 +0000\\\"\\n)\"" + }, + { + "key": "key_with_arr_val_nested", + "value": "[\"v1\",\"v2\",\"v3\",[\"nv1\",\"nv2\"]]" + }, + { + "key": "key_with_dict_val", + "value": "{\"k1\":\"v1\",\"k2\":\"v2\"}" + }, + { + "key": "key_with_date_val", + "value": "\"November 7, 2023 at 5:00:02 PM GMT\"" + }, + { + "key": "key_with_dict_val_nested", + "value": "\"{\\n k1 = v1;\\n k2 = v2;\\n k3 = {\\n nk1 = nv1;\\n nk2 = \\\"2023-11-07 17:00:02 +0000\\\";\\n };\\n}\"" + }, + { + "key": "key_with_num_val", + "value": "1234" + }, + { + "key": "key_with_str_val", + "value": "\"bar\"" + } + ] } diff --git a/Source/santad/testdata/protobuf/v6/exec.json b/Source/santad/testdata/protobuf/v6/exec.json index 455d9af41..c55d46e86 100644 --- a/Source/santad/testdata/protobuf/v6/exec.json +++ b/Source/santad/testdata/protobuf/v6/exec.json @@ -197,5 +197,43 @@ } }, "explain": "extra!", - "quarantine_url": "google.com" + "quarantine_url": "google.com", + "entitlements": [ + { + "key": "key_with_arr_val", + "value": "[\"v1\",\"v2\",\"v3\"]" + }, + { + "key": "key_with_data_val", + "value": "\"SGVsbG8gV29ybGQ=\"" + }, + { + "key": "key_with_arr_val_multitype", + "value": "\"(\\n v1,\\n v2,\\n v3,\\n 123,\\n \\\"2023-11-07 17:00:02 +0000\\\"\\n)\"" + }, + { + "key": "key_with_arr_val_nested", + "value": "[\"v1\",\"v2\",\"v3\",[\"nv1\",\"nv2\"]]" + }, + { + "key": "key_with_dict_val", + "value": "{\"k1\":\"v1\",\"k2\":\"v2\"}" + }, + { + "key": "key_with_date_val", + "value": "\"November 7, 2023 at 5:00:02 PM GMT\"" + }, + { + "key": "key_with_dict_val_nested", + "value": "\"{\\n k1 = v1;\\n k2 = v2;\\n k3 = {\\n nk1 = nv1;\\n nk2 = \\\"2023-11-07 17:00:02 +0000\\\";\\n };\\n}\"" + }, + { + "key": "key_with_num_val", + "value": "1234" + }, + { + "key": "key_with_str_val", + "value": "\"bar\"" + } + ] } From 8f3dbcd30a1e491b7e5c6f0e459c53f3381bcd3c Mon Sep 17 00:00:00 2001 From: Matt White <436037+mlw@users.noreply.github.com> Date: Wed, 8 Nov 2023 13:35:57 -0500 Subject: [PATCH 2/2] Standardize entitlement dictionary formatting --- Source/common/santa.proto | 4 +- .../EndpointSecurity/Serializers/Protobuf.mm | 53 ++++++++++++++----- .../Serializers/ProtobufTest.mm | 3 ++ Source/santad/testdata/protobuf/v1/exec.json | 18 +++---- Source/santad/testdata/protobuf/v2/exec.json | 18 +++---- Source/santad/testdata/protobuf/v4/exec.json | 18 +++---- Source/santad/testdata/protobuf/v5/exec.json | 18 +++---- Source/santad/testdata/protobuf/v6/exec.json | 18 +++---- 8 files changed, 90 insertions(+), 60 deletions(-) diff --git a/Source/common/santa.proto b/Source/common/santa.proto index e09c6df72..fc7da670c 100644 --- a/Source/common/santa.proto +++ b/Source/common/santa.proto @@ -214,8 +214,8 @@ message CertificateInfo { } message Entitlement { - string key = 1; - string value = 2; + string key = 1; + string value = 2; } // Information about a process execution event diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm b/Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm index 0d0808c8b..1db360d7c 100644 --- a/Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm +++ b/Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm @@ -72,6 +72,7 @@ namespace santa::santad::logs::endpoint_security::serializers { static constexpr NSUInteger kMaxEncodeObjectEntries = 64; +static constexpr NSUInteger kMaxEncodeObjectLevels = 5; std::shared_ptr Protobuf::Create(std::shared_ptr esapi, SNTDecisionCache *decision_cache, bool json) { @@ -451,11 +452,49 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info, return FinalizeProto(santa_msg); } +id StandardizedNestedObjects(id obj, int level) { + if (level-- == 0) { + return [obj description]; + } + + if ([obj isKindOfClass:[NSNumber class]] || [obj isKindOfClass:[NSString class]]) { + return obj; + } else if ([obj isKindOfClass:[NSArray class]]) { + NSMutableArray *arr = [NSMutableArray array]; + for (id item in obj) { + [arr addObject:StandardizedNestedObjects(item, level)]; + } + return arr; + } else if ([obj isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + for (id key in obj) { + [dict setObject:StandardizedNestedObjects(obj[key], level) forKey:key]; + } + return dict; + } else if ([obj isKindOfClass:[NSData class]]) { + return [obj base64EncodedStringWithOptions:0]; + } else if ([obj isKindOfClass:[NSDate class]]) { + return [NSISO8601DateFormatter stringFromDate:obj + timeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"] + formatOptions:NSISO8601DateFormatWithFractionalSeconds | NSISO8601DateFormatWithInternetDateTime]; + + } else { + NSLog(@"Got unknown... %d", level); + LOGW(@"Unexpected object encountered: %@", obj); + return [obj description]; + } +} + void EncodeEntitlements(::pbv1::Execution *pb_exec, NSDictionary *entitlements) { if (!entitlements) { return; } + // Since nested objects with varying types is hard for the API to serialize to + // JSON, first go through and standardize types to ensure better serialization + // as well as a consitent view of data. + entitlements = StandardizedNestedObjects(entitlements, kMaxEncodeObjectLevels); + __block int numObjectsToEncode = (int)std::min(kMaxEncodeObjectEntries, entitlements.count); pb_exec->mutable_entitlements()->Reserve(numObjectsToEncode); @@ -474,19 +513,7 @@ void EncodeEntitlements(::pbv1::Execution *pb_exec, NSDictionary *entitlements) NSError *err; NSData *jsonData; @try { - id val = obj; - - // Fixup some types with data that can be better represented in JSON - if ([obj isKindOfClass:[NSDate class]]) { - // Example format output: "November 6, 2023 at 10:25:20 AM EST" - val = [NSDateFormatter localizedStringFromDate:obj - dateStyle:NSDateFormatterLongStyle - timeStyle:NSDateFormatterLongStyle]; - } else if ([obj isKindOfClass:[NSData class]]) { - val = [obj base64EncodedStringWithOptions:0]; - } - - jsonData = [NSJSONSerialization dataWithJSONObject:val + jsonData = [NSJSONSerialization dataWithJSONObject:obj options:NSJSONWritingFragmentsAllowed error:&err]; } @catch (NSException *e) { diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm b/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm index afd9d78dd..d98925d11 100644 --- a/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm +++ b/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm @@ -276,7 +276,10 @@ void SerializeAndCheck(es_event_type_t eventType, options:NSJSONReadingMutableContainers error:&jsonError]; XCTAssertNil(jsonError, @"failed to parse got data as JSON"); + XCTAssertNil(FindDelta(wantJSONDict, gotJSONDict)); + // Note: Uncomment this line to help create testfile JSON when the assert above fails + // XCTAssertEqualObjects([NSString stringWithUTF8String:gotData.c_str()], wantData); } XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); diff --git a/Source/santad/testdata/protobuf/v1/exec.json b/Source/santad/testdata/protobuf/v1/exec.json index bec33b2f9..72f1760cb 100644 --- a/Source/santad/testdata/protobuf/v1/exec.json +++ b/Source/santad/testdata/protobuf/v1/exec.json @@ -123,16 +123,16 @@ "quarantine_url": "google.com", "entitlements": [ { - "key": "key_with_arr_val", - "value": "[\"v1\",\"v2\",\"v3\"]" + "key": "key_with_arr_val_multitype", + "value": "[\"v1\",\"v2\",\"v3\",123,\"2023-11-07T17:00:02.000Z\"]" }, { "key": "key_with_data_val", "value": "\"SGVsbG8gV29ybGQ=\"" }, { - "key": "key_with_arr_val_multitype", - "value": "\"(\\n v1,\\n v2,\\n v3,\\n 123,\\n \\\"2023-11-07 17:00:02 +0000\\\"\\n)\"" + "key": "key_with_str_val", + "value": "\"bar\"" }, { "key": "key_with_arr_val_nested", @@ -140,23 +140,23 @@ }, { "key": "key_with_dict_val", - "value": "{\"k1\":\"v1\",\"k2\":\"v2\"}" + "value": "{\"k2\":\"v2\",\"k1\":\"v1\"}" }, { "key": "key_with_date_val", - "value": "\"November 7, 2023 at 5:00:02 PM GMT\"" + "value": "\"2023-11-07T17:00:02.000Z\"" }, { "key": "key_with_dict_val_nested", - "value": "\"{\\n k1 = v1;\\n k2 = v2;\\n k3 = {\\n nk1 = nv1;\\n nk2 = \\\"2023-11-07 17:00:02 +0000\\\";\\n };\\n}\"" + "value": "{\"k3\":{\"nk1\":\"nv1\",\"nk2\":\"2023-11-07T17:00:02.000Z\"},\"k2\":\"v2\",\"k1\":\"v1\"}" }, { "key": "key_with_num_val", "value": "1234" }, { - "key": "key_with_str_val", - "value": "\"bar\"" + "key": "key_with_arr_val", + "value": "[\"v1\",\"v2\",\"v3\"]" } ] } diff --git a/Source/santad/testdata/protobuf/v2/exec.json b/Source/santad/testdata/protobuf/v2/exec.json index 4340a488e..95f2d81ac 100644 --- a/Source/santad/testdata/protobuf/v2/exec.json +++ b/Source/santad/testdata/protobuf/v2/exec.json @@ -151,16 +151,16 @@ "quarantine_url": "google.com", "entitlements": [ { - "key": "key_with_arr_val", - "value": "[\"v1\",\"v2\",\"v3\"]" + "key": "key_with_arr_val_multitype", + "value": "[\"v1\",\"v2\",\"v3\",123,\"2023-11-07T17:00:02.000Z\"]" }, { "key": "key_with_data_val", "value": "\"SGVsbG8gV29ybGQ=\"" }, { - "key": "key_with_arr_val_multitype", - "value": "\"(\\n v1,\\n v2,\\n v3,\\n 123,\\n \\\"2023-11-07 17:00:02 +0000\\\"\\n)\"" + "key": "key_with_str_val", + "value": "\"bar\"" }, { "key": "key_with_arr_val_nested", @@ -168,23 +168,23 @@ }, { "key": "key_with_dict_val", - "value": "{\"k1\":\"v1\",\"k2\":\"v2\"}" + "value": "{\"k2\":\"v2\",\"k1\":\"v1\"}" }, { "key": "key_with_date_val", - "value": "\"November 7, 2023 at 5:00:02 PM GMT\"" + "value": "\"2023-11-07T17:00:02.000Z\"" }, { "key": "key_with_dict_val_nested", - "value": "\"{\\n k1 = v1;\\n k2 = v2;\\n k3 = {\\n nk1 = nv1;\\n nk2 = \\\"2023-11-07 17:00:02 +0000\\\";\\n };\\n}\"" + "value": "{\"k3\":{\"nk1\":\"nv1\",\"nk2\":\"2023-11-07T17:00:02.000Z\"},\"k2\":\"v2\",\"k1\":\"v1\"}" }, { "key": "key_with_num_val", "value": "1234" }, { - "key": "key_with_str_val", - "value": "\"bar\"" + "key": "key_with_arr_val", + "value": "[\"v1\",\"v2\",\"v3\"]" } ] } diff --git a/Source/santad/testdata/protobuf/v4/exec.json b/Source/santad/testdata/protobuf/v4/exec.json index c55d46e86..4781ef27b 100644 --- a/Source/santad/testdata/protobuf/v4/exec.json +++ b/Source/santad/testdata/protobuf/v4/exec.json @@ -200,16 +200,16 @@ "quarantine_url": "google.com", "entitlements": [ { - "key": "key_with_arr_val", - "value": "[\"v1\",\"v2\",\"v3\"]" + "key": "key_with_arr_val_multitype", + "value": "[\"v1\",\"v2\",\"v3\",123,\"2023-11-07T17:00:02.000Z\"]" }, { "key": "key_with_data_val", "value": "\"SGVsbG8gV29ybGQ=\"" }, { - "key": "key_with_arr_val_multitype", - "value": "\"(\\n v1,\\n v2,\\n v3,\\n 123,\\n \\\"2023-11-07 17:00:02 +0000\\\"\\n)\"" + "key": "key_with_str_val", + "value": "\"bar\"" }, { "key": "key_with_arr_val_nested", @@ -217,23 +217,23 @@ }, { "key": "key_with_dict_val", - "value": "{\"k1\":\"v1\",\"k2\":\"v2\"}" + "value": "{\"k2\":\"v2\",\"k1\":\"v1\"}" }, { "key": "key_with_date_val", - "value": "\"November 7, 2023 at 5:00:02 PM GMT\"" + "value": "\"2023-11-07T17:00:02.000Z\"" }, { "key": "key_with_dict_val_nested", - "value": "\"{\\n k1 = v1;\\n k2 = v2;\\n k3 = {\\n nk1 = nv1;\\n nk2 = \\\"2023-11-07 17:00:02 +0000\\\";\\n };\\n}\"" + "value": "{\"k3\":{\"nk1\":\"nv1\",\"nk2\":\"2023-11-07T17:00:02.000Z\"},\"k2\":\"v2\",\"k1\":\"v1\"}" }, { "key": "key_with_num_val", "value": "1234" }, { - "key": "key_with_str_val", - "value": "\"bar\"" + "key": "key_with_arr_val", + "value": "[\"v1\",\"v2\",\"v3\"]" } ] } diff --git a/Source/santad/testdata/protobuf/v5/exec.json b/Source/santad/testdata/protobuf/v5/exec.json index b55e137ea..e6b6ba94f 100644 --- a/Source/santad/testdata/protobuf/v5/exec.json +++ b/Source/santad/testdata/protobuf/v5/exec.json @@ -200,16 +200,16 @@ "quarantine_url": "google.com", "entitlements": [ { - "key": "key_with_arr_val", - "value": "[\"v1\",\"v2\",\"v3\"]" + "key": "key_with_arr_val_multitype", + "value": "[\"v1\",\"v2\",\"v3\",123,\"2023-11-07T17:00:02.000Z\"]" }, { "key": "key_with_data_val", "value": "\"SGVsbG8gV29ybGQ=\"" }, { - "key": "key_with_arr_val_multitype", - "value": "\"(\\n v1,\\n v2,\\n v3,\\n 123,\\n \\\"2023-11-07 17:00:02 +0000\\\"\\n)\"" + "key": "key_with_str_val", + "value": "\"bar\"" }, { "key": "key_with_arr_val_nested", @@ -217,23 +217,23 @@ }, { "key": "key_with_dict_val", - "value": "{\"k1\":\"v1\",\"k2\":\"v2\"}" + "value": "{\"k2\":\"v2\",\"k1\":\"v1\"}" }, { "key": "key_with_date_val", - "value": "\"November 7, 2023 at 5:00:02 PM GMT\"" + "value": "\"2023-11-07T17:00:02.000Z\"" }, { "key": "key_with_dict_val_nested", - "value": "\"{\\n k1 = v1;\\n k2 = v2;\\n k3 = {\\n nk1 = nv1;\\n nk2 = \\\"2023-11-07 17:00:02 +0000\\\";\\n };\\n}\"" + "value": "{\"k3\":{\"nk1\":\"nv1\",\"nk2\":\"2023-11-07T17:00:02.000Z\"},\"k2\":\"v2\",\"k1\":\"v1\"}" }, { "key": "key_with_num_val", "value": "1234" }, { - "key": "key_with_str_val", - "value": "\"bar\"" + "key": "key_with_arr_val", + "value": "[\"v1\",\"v2\",\"v3\"]" } ] } diff --git a/Source/santad/testdata/protobuf/v6/exec.json b/Source/santad/testdata/protobuf/v6/exec.json index c55d46e86..4781ef27b 100644 --- a/Source/santad/testdata/protobuf/v6/exec.json +++ b/Source/santad/testdata/protobuf/v6/exec.json @@ -200,16 +200,16 @@ "quarantine_url": "google.com", "entitlements": [ { - "key": "key_with_arr_val", - "value": "[\"v1\",\"v2\",\"v3\"]" + "key": "key_with_arr_val_multitype", + "value": "[\"v1\",\"v2\",\"v3\",123,\"2023-11-07T17:00:02.000Z\"]" }, { "key": "key_with_data_val", "value": "\"SGVsbG8gV29ybGQ=\"" }, { - "key": "key_with_arr_val_multitype", - "value": "\"(\\n v1,\\n v2,\\n v3,\\n 123,\\n \\\"2023-11-07 17:00:02 +0000\\\"\\n)\"" + "key": "key_with_str_val", + "value": "\"bar\"" }, { "key": "key_with_arr_val_nested", @@ -217,23 +217,23 @@ }, { "key": "key_with_dict_val", - "value": "{\"k1\":\"v1\",\"k2\":\"v2\"}" + "value": "{\"k2\":\"v2\",\"k1\":\"v1\"}" }, { "key": "key_with_date_val", - "value": "\"November 7, 2023 at 5:00:02 PM GMT\"" + "value": "\"2023-11-07T17:00:02.000Z\"" }, { "key": "key_with_dict_val_nested", - "value": "\"{\\n k1 = v1;\\n k2 = v2;\\n k3 = {\\n nk1 = nv1;\\n nk2 = \\\"2023-11-07 17:00:02 +0000\\\";\\n };\\n}\"" + "value": "{\"k3\":{\"nk1\":\"nv1\",\"nk2\":\"2023-11-07T17:00:02.000Z\"},\"k2\":\"v2\",\"k1\":\"v1\"}" }, { "key": "key_with_num_val", "value": "1234" }, { - "key": "key_with_str_val", - "value": "\"bar\"" + "key": "key_with_arr_val", + "value": "[\"v1\",\"v2\",\"v3\"]" } ] }