Skip to content

Commit

Permalink
Add basic support for importing and exporting rules to/from JSON (#1170)
Browse files Browse the repository at this point in the history
* Add basic support for importing and exporting rules to/from JSON.
  • Loading branch information
pmarkowsky authored Sep 13, 2023
1 parent ff6bf07 commit b8d7ed0
Show file tree
Hide file tree
Showing 10 changed files with 321 additions and 1 deletion.
5 changes: 5 additions & 0 deletions Source/common/SNTRule.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,9 @@
///
- (void)resetTimestamp;

///
/// Returns a dictionary representation of the rule.
///
- (NSDictionary *)dictionaryRepresentation;

@end
37 changes: 37 additions & 0 deletions Source/common/SNTRule.m
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,43 @@ - (instancetype)initWithCoder:(NSCoder *)decoder {
return self;
}

- (NSString *)ruleStateToPolicyString:(SNTRuleState)state {
switch (state) {
case SNTRuleStateAllow: return kRulePolicyAllowlist;
case SNTRuleStateAllowCompiler: return kRulePolicyAllowlistCompiler;
case SNTRuleStateBlock: return kRulePolicyBlocklist;
case SNTRuleStateSilentBlock: return kRulePolicySilentBlocklist;
case SNTRuleStateRemove: return kRulePolicyRemove;
case SNTRuleStateAllowTransitive: return @"AllowTransitive";
// This should never be hit. But is here for completion.
default: return @"Unknown";
}
}

- (NSString *)ruleTypeToString:(SNTRuleType)ruleType {
switch (ruleType) {
case SNTRuleTypeBinary: return kRuleTypeBinary;
case SNTRuleTypeCertificate: return kRuleTypeCertificate;
case SNTRuleTypeTeamID: return kRuleTypeTeamID;
case SNTRuleTypeSigningID: return kRuleTypeSigningID;
// This should never be hit. If we have rule types of Unknown then there's a
// coding error somewhere.
default: return @"Unknown";
}
}

// Returns an NSDictionary representation of the rule. Primarily use for
// exporting rules.
- (NSDictionary *)dictionaryRepresentation {
return @{
kRuleIdentifier : self.identifier,
kRulePolicy : [self ruleStateToPolicyString:self.state],
kRuleType : [self ruleTypeToString:self.type],
kRuleCustomMsg : self.customMsg ?: @"",
kRuleCustomURL : self.customURL ?: @""
};
}

#undef DECODE
#undef ENCODE
#pragma clang diagnostic pop
Expand Down
62 changes: 62 additions & 0 deletions Source/common/SNTRuleTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#import <XCTest/XCTest.h>
#include "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTSyncConstants.h"

#import "Source/common/SNTRule.h"

Expand Down Expand Up @@ -95,12 +96,14 @@ - (void)testInitWithDictionaryValid {
@"policy" : @"ALLOWLIST",
@"rule_type" : @"TEAMID",
@"custom_msg" : @"A custom block message",
@"custom_url" : @"https://example.com",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"ABCDEFGHIJ");
XCTAssertEqual(sut.type, SNTRuleTypeTeamID);
XCTAssertEqual(sut.state, SNTRuleStateAllow);
XCTAssertEqualObjects(sut.customMsg, @"A custom block message");
XCTAssertEqualObjects(sut.customURL, @"https://example.com");

// TeamIDs must be 10 chars in length
sut = [[SNTRule alloc] initWithDictionary:@{
Expand Down Expand Up @@ -222,4 +225,63 @@ - (void)testInitWithDictionaryInvalid {
XCTAssertNil(sut);
}

- (void)testRuleDictionaryRepresentation {
NSDictionary *expectedTeamID = @{
@"identifier" : @"ABCDEFGHIJ",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"TEAMID",
@"custom_msg" : @"A custom block message",
@"custom_url" : @"https://example.com",
};

SNTRule *sut = [[SNTRule alloc] initWithDictionary:expectedTeamID];
NSDictionary *dict = [sut dictionaryRepresentation];
XCTAssertEqualObjects(expectedTeamID, dict);

NSDictionary *expectedBinary = @{
@"identifier" : @"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6",
@"policy" : @"BLOCKLIST",
@"rule_type" : @"BINARY",
@"custom_msg" : @"",
@"custom_url" : @"",
};

sut = [[SNTRule alloc] initWithDictionary:expectedBinary];
dict = [sut dictionaryRepresentation];

XCTAssertEqualObjects(expectedBinary, dict);
}

- (void)testRuleStateToPolicyString {
NSDictionary *expected = @{
@"identifier" : @"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"BINARY",
@"custom_msg" : @"A custom block message",
@"custom_url" : @"https://example.com",
};

SNTRule *sut = [[SNTRule alloc] initWithDictionary:expected];
sut.state = SNTRuleStateBlock;
XCTAssertEqualObjects(kRulePolicyBlocklist, [sut dictionaryRepresentation][kRulePolicy]);
sut.state = SNTRuleStateSilentBlock;
XCTAssertEqualObjects(kRulePolicySilentBlocklist, [sut dictionaryRepresentation][kRulePolicy]);
sut.state = SNTRuleStateAllow;
XCTAssertEqualObjects(kRulePolicyAllowlist, [sut dictionaryRepresentation][kRulePolicy]);
sut.state = SNTRuleStateAllowCompiler;
XCTAssertEqualObjects(kRulePolicyAllowlistCompiler, [sut dictionaryRepresentation][kRulePolicy]);
// Invalid states
sut.state = SNTRuleStateRemove;
XCTAssertEqualObjects(kRulePolicyRemove, [sut dictionaryRepresentation][kRulePolicy]);
}

/*
- (void)testRuleTypeToString {
SNTRule *sut = [[SNTRule alloc] init];
XCTAssertEqual(kRuleTypeBinary, [sut ruleTypeToString:@""]);//SNTRuleTypeBinary]);
XCTAssertEqual(kRuleTypeCertificate,[sut ruleTypeToString:SNTRuleTypeCertificate]);
XCTAssertEqual(kRuleTypeTeamID, [sut ruleTypeToString:SNTRuleTypeTeamID]);
XCTAssertEqual(kRuleTypeSigningID,[sut ruleTypeToString:SNTRuleTypeSigningID]);
}*/

@end
1 change: 1 addition & 0 deletions Source/common/SNTXPCControlInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
teamID:(NSString *)teamID
signingID:(NSString *)signingID
reply:(void (^)(SNTRule *))reply;
- (void)retrieveAllRules:(void (^)(NSArray<SNTRule *> *rules, NSError *error))reply;

///
/// Config ops
Expand Down
5 changes: 5 additions & 0 deletions Source/common/SNTXPCControlInterface.m
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ + (void)initializeControlInterface:(NSXPCInterface *)r {
forSelector:@selector(databaseRuleAddRules:cleanSlate:reply:)
argumentIndex:0
ofReply:NO];

[r setClasses:[NSSet setWithObjects:[NSArray class], [SNTRule class], nil]
forSelector:@selector(retrieveAllRules:)
argumentIndex:0
ofReply:YES];
}

+ (NSXPCInterface *)controlInterface {
Expand Down
156 changes: 155 additions & 1 deletion Source/santactl/Commands/SNTCommandRule.m
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ + (NSString *)longHelpText {
@" --compiler: allow and mark as a compiler\n"
@" --remove: remove existing rule\n"
@" --check: check for an existing rule\n"
@" --import: import rules from a JSON file\n"
@" --export: export rules to a JSON file\n"
@"\n"
@" One of:\n"
@" --path {path}: path of binary/bundle to add/remove.\n"
Expand All @@ -62,6 +64,7 @@ + (NSString *)longHelpText {
@" the rule state of a file.\n"
@" --identifier {sha256|teamID|signingID}: identifier to add/remove/check\n"
@" --sha256 {sha256}: hash to add/remove/check [deprecated]\n"
@" --json {path}: path to a JSON file containing a list of rules to add/remove\n"
@"\n"
@" Optionally:\n"
@" --teamid: add or check a team ID rule instead of binary\n"
Expand All @@ -81,7 +84,21 @@ + (NSString *)longHelpText {
@" that the signing ID is properly scoped to a developer. For the special\n"
@" case of platform binaries, `TeamID` should be replaced with the string\n"
@" \"platform\" (e.g. `platform:SigningID`). This allows for rules\n"
@" targeting Apple-signed binaries that do not have a team ID.\n");
@" targeting Apple-signed binaries that do not have a team ID.\n"
@"\n"
@" Importing / Exporting Rules:\n"
@" If santa is not configured to use a sync server one can export\n"
@" & import its non-static rules to and from JSON files using the \n"
@" --export/--import flags. These files have the following form:\n"
@"\n"
@" {\"rules\": [{rule-dictionaries}]}\n"
@" e.g. {\"rules\": [\n"
@" {\"policy\": \"BLOCKLIST\",\n"
@" \"identifier\": "
@"\"84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6\"\n"
@" \"custom_url\" : \"\",\n"
@" \"custom_msg\": \"/bin/ls block for demo\"}\n"
@" ]}\n");
}

- (void)runWithArguments:(NSArray *)arguments {
Expand All @@ -103,7 +120,10 @@ - (void)runWithArguments:(NSArray *)arguments {
newRule.type = SNTRuleTypeBinary;

NSString *path;
NSString *jsonFilePath;
BOOL check = NO;
BOOL importRules = NO;
BOOL exportRules = NO;

// Parse arguments
for (NSUInteger i = 0; i < arguments.count; ++i) {
Expand Down Expand Up @@ -154,6 +174,33 @@ - (void)runWithArguments:(NSArray *)arguments {
} else if ([arg caseInsensitiveCompare:@"--force"] == NSOrderedSame) {
// Don't do anything special.
#endif
} else if ([arg caseInsensitiveCompare:@"--json"] == NSOrderedSame) {
if (++i > arguments.count - 1) {
[self printErrorUsageAndExit:@"--json requires an argument"];
}
jsonFilePath = arguments[i];
} else if ([arg caseInsensitiveCompare:@"--import"] == NSOrderedSame) {
if (exportRules) {
[self printErrorUsageAndExit:@"--import and --export are mutually exclusive"];
}
importRules = YES;
if (++i > arguments.count - 1) {
[self printErrorUsageAndExit:@"--import requires an argument"];
}
jsonFilePath = arguments[i];
} else if ([arg caseInsensitiveCompare:@"--export"] == NSOrderedSame) {
if (importRules) {
[self printErrorUsageAndExit:@"--import and --export are mutually exclusive"];
}
exportRules = YES;
if (++i > arguments.count - 1) {
[self printErrorUsageAndExit:@"--export requires an argument"];
}
jsonFilePath = arguments[i];
} else if ([arg caseInsensitiveCompare:@"--help"] == NSOrderedSame ||
[arg caseInsensitiveCompare:@"-h"] == NSOrderedSame) {
printf("%s\n", self.class.longHelpText.UTF8String);
exit(0);
} else {
[self printErrorUsageAndExit:[@"Unknown argument: " stringByAppendingString:arg]];
}
Expand Down Expand Up @@ -189,6 +236,16 @@ - (void)runWithArguments:(NSArray *)arguments {
return [self printStateOfRule:newRule daemonConnection:self.daemonConn];
}

// Note this block needs to come after the check block above.
if (jsonFilePath.length > 0) {
if (importRules) {
[self importJSONFile:jsonFilePath];
} else if (exportRules) {
[self exportJSONFile:jsonFilePath];
}
return;
}

if (newRule.state == SNTRuleStateUnknown) {
[self printErrorUsageAndExit:@"No state specified"];
} else if (!newRule.identifier) {
Expand Down Expand Up @@ -302,4 +359,101 @@ - (void)printStateOfRule:(SNTRule *)rule daemonConnection:(MOLXPCConnection *)da
exit(0);
}

- (void)importJSONFile:(NSString *)jsonFilePath {
// If the file exists parse it and then add the rules one at a time.
NSError *error;
NSData *data = [NSData dataWithContentsOfFile:jsonFilePath options:0 error:&error];
if (error) {
[self printErrorUsageAndExit:[NSString stringWithFormat:@"Failed to read %@: %@", jsonFilePath,
error.localizedDescription]];
}

// We expect a JSON object with one key "rules". This is an array of rule
// objects.
// e.g.
// {"rules": [{
// "policy" : "BLOCKLIST",
// "rule_type" : "BINARY",
// "identifier" : "84de9c61777ca36b13228e2446d53e966096e78db7a72c632b5c185b2ffe68a6"
// "custom_url" : "",
// "custom_msg" : "/bin/ls block for demo"
// }]}
NSDictionary *rules = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (error) {
[self printErrorUsageAndExit:[NSString stringWithFormat:@"Failed to parse %@: %@", jsonFilePath,
error.localizedDescription]];
}

NSMutableArray<SNTRule *> *parsedRules = [[NSMutableArray alloc] init];

for (NSDictionary *jsonRule in rules[@"rules"]) {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:jsonRule];
if (!rule) {
[self printErrorUsageAndExit:[NSString stringWithFormat:@"Invalid rule: %@", jsonRule]];
}
[parsedRules addObject:rule];
}

[[self.daemonConn remoteObjectProxy]
databaseRuleAddRules:parsedRules
cleanSlate:NO
reply:^(NSError *error) {
if (error) {
printf("Failed to modify rules: %s",
[error.localizedDescription UTF8String]);
LOGD(@"Failure reason: %@", error.localizedFailureReason);
exit(1);
}
exit(0);
}];
}

- (void)exportJSONFile:(NSString *)jsonFilePath {
// Get the rules from the daemon and then write them to the file.
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
[rop retrieveAllRules:^(NSArray<SNTRule *> *rules, NSError *error) {
if (error) {
printf("Failed to get rules: %s", [error.localizedDescription UTF8String]);
LOGD(@"Failure reason: %@", error.localizedFailureReason);
exit(1);
}

if (rules.count == 0) {
printf("No rules to export.\n");
exit(1);
}
// Convert Rules to an NSDictionary.
NSMutableArray *rulesAsDicts = [[NSMutableArray alloc] init];

for (SNTRule *rule in rules) {
// Omit transitive and remove rules as they're not relevan.
if (rule.state == SNTRuleStateAllowTransitive || rule.state == SNTRuleStateRemove) {
continue;
}

[rulesAsDicts addObject:[rule dictionaryRepresentation]];
}

NSOutputStream *outputStream = [[NSOutputStream alloc] initToFileAtPath:jsonFilePath append:NO];
[outputStream open];

// Write the rules to the file.
// File should look like the following JSON:
// {"rules": [{"policy": "ALLOWLIST", "identifier": hash, "rule_type: "BINARY"},}]}
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:@{@"rules" : rulesAsDicts}
options:NSJSONWritingPrettyPrinted
error:&error];
// Print error
if (error) {
printf("Failed to jsonify rules: %s", [error.localizedDescription UTF8String]);
LOGD(@"Failure reason: %@", error.localizedFailureReason);
exit(1);
}
// Write jsonData to the file
[outputStream write:jsonData.bytes maxLength:jsonData.length];
[outputStream close];
exit(0);
}];
}

@end
5 changes: 5 additions & 0 deletions Source/santad/DataLayer/SNTRuleTable.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@
///
- (void)removeOutdatedTransitiveRules;

///
/// Retrieve all rules from the database for export.
///
- (NSArray<SNTRule *> *)retrieveAllRules;

///
/// A map of a file hashes to cached decisions. This is used to pre-validate and whitelist
/// certain critical system binaries that are integral to Santa's functionality.
Expand Down
15 changes: 15 additions & 0 deletions Source/santad/DataLayer/SNTRuleTable.m
Original file line number Diff line number Diff line change
Expand Up @@ -539,4 +539,19 @@ - (BOOL)fillError:(NSError **)error code:(SNTRuleTableError)code message:(NSStri
return YES;
}

#pragma mark Querying

// Retrieve all rules from the Database
- (NSArray<SNTRule *> *)retrieveAllRules {
NSMutableArray<SNTRule *> *rules = [NSMutableArray array];
[self inDatabase:^(FMDatabase *db) {
FMResultSet *rs = [db executeQuery:@"SELECT * FROM rules"];
while ([rs next]) {
[rules addObject:[self ruleFromResultSet:rs]];
}
[rs close];
}];
return rules;
}

@end
Loading

0 comments on commit b8d7ed0

Please sign in to comment.