Skip to content

Commit

Permalink
Add support for logging entitlements in EXEC events
Browse files Browse the repository at this point in the history
  • Loading branch information
mlw committed Nov 7, 2023
1 parent 8f5f8de commit 98305a9
Show file tree
Hide file tree
Showing 15 changed files with 445 additions and 24 deletions.
6 changes: 6 additions & 0 deletions Source/common/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ objc_library(
],
)

objc_library(
name = "SNTDeepCopy",
srcs = ["SNTDeepCopy.m"],
hdrs = ["SNTDeepCopy.h"],
)

cc_library(
name = "SantaCache",
hdrs = ["SantaCache.h"],
Expand Down
1 change: 1 addition & 0 deletions Source/common/SNTCachedDecision.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
@property NSArray<MOLCertificate *> *certChain;
@property NSString *teamID;
@property NSString *signingID;
@property NSDictionary *entitlements;

@property NSString *quarantineURL;

Expand Down
27 changes: 27 additions & 0 deletions Source/common/SNTDeepCopy.h
Original file line number Diff line number Diff line change
@@ -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 <Foundation/Foundation.h>

@interface NSArray (SNTDeepCopy)

- (instancetype)sntDeepCopy;

@end

@interface NSDictionary (SNTDeepCopy)

- (instancetype)sntDeepCopy;

@end
53 changes: 53 additions & 0 deletions Source/common/SNTDeepCopy.m
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion Source/common/TestUtils.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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, *)) {
Expand Down
10 changes: 10 additions & 0 deletions Source/common/santa.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Source/santad/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
87 changes: 87 additions & 0 deletions Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@

namespace santa::santad::logs::endpoint_security::serializers {

static constexpr NSUInteger kMaxEncodeObjectEntries = 64;

std::shared_ptr<Protobuf> Protobuf::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
SNTDecisionCache *decision_cache, bool json) {
return std::make_shared<Protobuf>(esapi, std::move(decision_cache), json);
Expand Down Expand Up @@ -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<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCachedDecision *cd) {
Arena arena;
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
Expand Down Expand Up @@ -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);
}

Expand Down
74 changes: 56 additions & 18 deletions Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -166,28 +168,35 @@ bool CompareTime(const Timestamp &timestamp, 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,
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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<MockEndpointSecurityAPI> mockESApi,
Expand Down
Loading

0 comments on commit 98305a9

Please sign in to comment.