Skip to content

Commit

Permalink
Add entitlements to sync protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
mlw committed Dec 9, 2024
1 parent 82fddd3 commit 0ce39e1
Show file tree
Hide file tree
Showing 13 changed files with 405 additions and 132 deletions.
2 changes: 1 addition & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ bazel_dep(name = "googletest", version = "1.14.0.bcr.1", repo_name = "com_google
bazel_dep(name = "protos", version = "1.0.1", repo_name = "northpole_protos")
git_override(
module_name = "protos",
commit = "86aea2de00862d5f24272d865586ce7e805a4f18",
commit = "b2d1e1214440ba42d129338c1f1e6466a83f30d9",
remote = "https://github.com/northpolesec/protos",
)

Expand Down
19 changes: 19 additions & 0 deletions Source/common/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,24 @@ objc_library(
],
)

objc_library(
name = "EncodeEntitlements",
srcs = ["EncodeEntitlements.mm"],
hdrs = ["EncodeEntitlements.h"],
deps = [
":SNTLogging",
],
)

santa_unit_test(
name = "EncodeEntitlementsTest",
srcs = ["EncodeEntitlementsTest.mm"],
deps = [
":EncodeEntitlements",
],
)


objc_library(
name = "SigningIDHelpers",
srcs = ["SigningIDHelpers.m"],
Expand Down Expand Up @@ -536,6 +554,7 @@ santa_unit_test(
test_suite(
name = "unit_tests",
tests = [
":EncodeEntitlementsTest",
":PrefixTreeTest",
":SNTBlockMessageTest",
":SNTCachedDecisionTest",
Expand Down
29 changes: 29 additions & 0 deletions Source/common/EncodeEntitlements.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/// Copyright 2024 North Pole Security, Inc.
///
/// 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__COMMON__ENCODEENTITLEMENTS_H
#define SANTA__COMMON__ENCODEENTITLEMENTS_H

#include <Foundation/Foundation.h>

namespace santa {

void EncodeEntitlementsCommon(NSDictionary *entitlements, BOOL entitlements_filtered,
void (^EncodeInitBlock)(NSUInteger count, bool is_filtered),
void (^EncodeEntitlementBlock)(NSString *entitlement,
NSString *value));

}

#endif
129 changes: 129 additions & 0 deletions Source/common/EncodeEntitlements.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/// Copyright 2024 North Pole Security, Inc.
///
/// 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/common/EncodeEntitlements.h"

#include <algorithm>

#include "Source/common/SNTLogging.h"

namespace santa {

static constexpr NSUInteger kMaxEncodeObjectEntries = 64;
static constexpr NSUInteger kMaxEncodeObjectLevels = 5;

id StandardizedNestedObjects(id obj, int level) {
if (!obj) {
return nil;
} else 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 {
LOGW(@"Unexpected object encountered: %@", obj);
return [obj description];
}
}

void EncodeEntitlementsCommon(NSDictionary *entitlements, BOOL entitlements_filtered,
void (^EncodeInitBlock)(NSUInteger count, bool is_filtered),
void (^EncodeEntitlementBlock)(NSString *entitlement,
NSString *value)) {
NSDictionary *standardized_entitlements =
StandardizedNestedObjects(entitlements, kMaxEncodeObjectLevels);
__block int num_objects_to_encode =
(int)std::min(kMaxEncodeObjectEntries, standardized_entitlements.count);

EncodeInitBlock(
num_objects_to_encode,
entitlements_filtered != NO || num_objects_to_encode != standardized_entitlements.count);

[standardized_entitlements enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if (num_objects_to_encode-- == 0) {
*stop = YES;
return;
}

if (![key isKindOfClass:[NSString class]]) {
LOGW(@"Skipping entitlement key with unexpected key type: %@", key);
return;
}

NSError *err;
NSData *json_data;
@try {
json_data = [NSJSONSerialization dataWithJSONObject:obj
options:NSJSONWritingFragmentsAllowed
error:&err];
} @catch (NSException *e) {
LOGW(@"Encountered entitlement that cannot directly convert to JSON: %@: %@", key, obj);
}

if (!json_data) {
// 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 {
json_data = [NSJSONSerialization dataWithJSONObject:[obj description]
options:NSJSONWritingFragmentsAllowed
error:&err];
} @catch (NSException *e) {
LOGW(@"Unable to create fallback string: %@: %@", key, obj);
}

if (!json_data) {
// As a final fallback, simply serialize an error message so that the
// entitlement key is still logged.
json_data = [NSJSONSerialization dataWithJSONObject:@"JSON Serialization Failed"
options:NSJSONWritingFragmentsAllowed
error:&err];
}
}

// This shouldn't be possible given the fallback code above. But handle it
// just in case to prevent a crash.
if (!json_data) {
LOGW(@"Failed to create valid JSON for entitlement: %@", key);
return;
}

EncodeEntitlementBlock(key, [[NSString alloc] initWithData:json_data
encoding:NSUTF8StringEncoding]);
}];
}

} // namespace santa
158 changes: 158 additions & 0 deletions Source/common/EncodeEntitlementsTest.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/// Copyright 2024 North Pole Security, Inc.
///
/// 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
///
/// http://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/common/EncodeEntitlements.h"
#include "XCTest/XCTest.h"

#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>

namespace santa {
extern id StandardizedNestedObjects(id obj, int level);
} // namespace santa

using santa::EncodeEntitlementsCommon;
using santa::StandardizedNestedObjects;

@interface EncodeEntitlementsTest : XCTestCase
@end

@implementation EncodeEntitlementsTest

- (void)testStandardizedNestedObjectsTypes {
id val = StandardizedNestedObjects(@"asdf", 1);
XCTAssertTrue([val isKindOfClass:[NSString class]]);

val = StandardizedNestedObjects(@(0), 1);
XCTAssertTrue([val isKindOfClass:[NSNumber class]]);

val = StandardizedNestedObjects(@[], 1);
XCTAssertTrue([val isKindOfClass:[NSArray class]]);

val = StandardizedNestedObjects(@{}, 1);
XCTAssertTrue([val isKindOfClass:[NSDictionary class]]);

val = StandardizedNestedObjects([[NSData alloc] init], 1);
XCTAssertTrue([val isKindOfClass:[NSString class]]);

val = StandardizedNestedObjects([NSDate now], 1);
XCTAssertTrue([val isKindOfClass:[NSString class]]);
}

- (void)testStandardizedNestedObjectsLevels {
NSArray *nestedObj = @[
@[
@[
@[ @"111", @"112" ],
@[ @"113", @"114" ],
],
@[
@[ @"121", @"122" ],
@[ @"123", @"124" ],
]
],
@[
@[
@[ @"211", @"212" ],
@[ @"213", @"214" ],
],
@[
@[ @"221", @"222" ],
@[ @"223", @"224" ],
]
]
];

id val = StandardizedNestedObjects(nestedObj, 1);

XCTAssertEqual(((NSArray *)val).count, 2);
XCTAssertEqualObjects(
val[0], @"(\n (\n (\n 111,\n 112\n ),\n "
@" (\n 113,\n 114\n )\n ),\n (\n "
@" (\n 121,\n 122\n ),\n "
@"(\n 123,\n 124\n )\n )\n)");
XCTAssertEqualObjects(
val[1], @"(\n (\n (\n 211,\n 212\n ),\n "
@" (\n 213,\n 214\n )\n ),\n (\n "
@" (\n 221,\n 222\n ),\n "
@"(\n 223,\n 224\n )\n )\n)");

val = StandardizedNestedObjects(nestedObj, 3);

XCTAssertEqual(((NSArray *)val).count, 2);
XCTAssertEqual(((NSArray *)val[0]).count, 2);
XCTAssertEqual(((NSArray *)val[1]).count, 2);
XCTAssertEqual(((NSArray *)val[0][0]).count, 2);
XCTAssertEqual(((NSArray *)val[0][1]).count, 2);
XCTAssertEqualObjects(val[0][0][0], @"(\n 111,\n 112\n)");
XCTAssertEqualObjects(val[0][0][1], @"(\n 113,\n 114\n)");
XCTAssertEqualObjects(val[0][1][0], @"(\n 121,\n 122\n)");
XCTAssertEqualObjects(val[0][1][1], @"(\n 123,\n 124\n)");
XCTAssertEqualObjects(val[1][0][0], @"(\n 211,\n 212\n)");
XCTAssertEqualObjects(val[1][0][1], @"(\n 213,\n 214\n)");
XCTAssertEqualObjects(val[1][1][0], @"(\n 221,\n 222\n)");
XCTAssertEqualObjects(val[1][1][1], @"(\n 223,\n 224\n)");
}

- (void)testEncodeEntitlementsCommonBasic {
NSDictionary *entitlements = @{
@"ent1" : @"val1",
@"ent2" : @"val2",
};

EncodeEntitlementsCommon(
entitlements, false,
^(NSUInteger count, bool is_filtered) {
XCTAssertEqual(count, entitlements.count);
XCTAssertFalse(is_filtered);
},
^(NSString *entitlement, NSString *value) {
if ([entitlement isEqualToString:@"ent1"]) {
XCTAssertEqualObjects(value, @"\"val1\"");
} else if ([entitlement isEqualToString:@"ent2"]) {
XCTAssertEqualObjects(value, @"\"val2\"");
} else {
XCTFail(@"Unexpected entitlement: %@", entitlement);
}
});
}

- (void)testEncodeEntitlementsCommonFiltered {
NSMutableDictionary *entitlements = [NSMutableDictionary dictionary];

EncodeEntitlementsCommon(entitlements, true,
^(NSUInteger count, bool is_filtered) {
XCTAssertEqual(count, entitlements.count);
XCTAssertTrue(is_filtered);
},
^(NSString *entitlement, NSString *value){
// noop
});

// Create a large dictionary that will get capped
for (int i = 0; i < 100; i++) {
entitlements[[NSString stringWithFormat:@"ent%d", i]] = [NSString stringWithFormat:@"val%d", i];
}

EncodeEntitlementsCommon(entitlements, false,
^(NSUInteger count, bool is_filtered) {
XCTAssertLessThan(count, entitlements.count);
XCTAssertTrue(is_filtered);
},
^(NSString *entitlement, NSString *value){
// noop
});
}

@end
10 changes: 10 additions & 0 deletions Source/common/SNTStoredEvent.h
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,14 @@
///
@property(readonly) NSArray *signingChainCertRefs;

///
/// If the executed file was entitled, this is the set of key/value pairs of entitlements
///
@property NSDictionary *entitlements;

///
/// Whether or not the set of entitlements were filtered (e.g. due to configuration)
///
@property BOOL entitlementsFiltered;

@end
Loading

0 comments on commit 0ce39e1

Please sign in to comment.