Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add entitlements to sync protocol #163

Merged
merged 1 commit into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading