From 9efe32938179b1d9452977cb4501a86133351dd2 Mon Sep 17 00:00:00 2001 From: Pete Markowsky Date: Sun, 3 Mar 2024 11:14:14 -0500 Subject: [PATCH] Update SNTPolicyProcessor to use a map instead of a giant switch statement Update SNTPolicyProcessor to use a map instead of a giant switch statement. Add unit tests for the method that sets SNTCachedDecision values. --- Source/santad/BUILD | 17 +- Source/santad/SNTPolicyProcessor.h | 10 + ...olicyProcessor.m => SNTPolicyProcessor.mm} | 273 ++++----- Source/santad/SNTPolicyProcessorTest.m | 564 ++++++++++++++++++ 4 files changed, 722 insertions(+), 142 deletions(-) rename Source/santad/{SNTPolicyProcessor.m => SNTPolicyProcessor.mm} (66%) create mode 100644 Source/santad/SNTPolicyProcessorTest.m diff --git a/Source/santad/BUILD b/Source/santad/BUILD index 7d3f3fe65..9d0c55f9d 100644 --- a/Source/santad/BUILD +++ b/Source/santad/BUILD @@ -193,7 +193,7 @@ objc_library( objc_library( name = "SNTPolicyProcessor", - srcs = ["SNTPolicyProcessor.m"], + srcs = ["SNTPolicyProcessor.mm"], hdrs = ["SNTPolicyProcessor.h"], deps = [ ":SNTRuleTable", @@ -209,7 +209,21 @@ objc_library( "@MOLCertificate", "@MOLCodesignChecker", "@MOLXPCConnection", + "@com_google_absl//absl/container:flat_hash_map", + ], +) + +santa_unit_test( + name = "SNTPolicyProcessorTest", + srcs = ["SNTPolicyProcessorTest.m"], + deps = [ + ":SNTPolicyProcessor", + "//Source/common:SNTConfigurator", + "//Source/common:SNTRule", + "//Source/common:TestUtils", + "@OCMock", ], + ) objc_library( @@ -1417,6 +1431,7 @@ test_suite( ":SNTEndpointSecurityTamperResistanceTest", ":SNTEventTableTest", ":SNTExecutionControllerTest", + ":SNTPolicyProcessorTest", ":SNTRuleTableTest", ":SantadTest", ":WatchItemsTest", diff --git a/Source/santad/SNTPolicyProcessor.h b/Source/santad/SNTPolicyProcessor.h index 6cefe5723..02d7fe808 100644 --- a/Source/santad/SNTPolicyProcessor.h +++ b/Source/santad/SNTPolicyProcessor.h @@ -18,6 +18,7 @@ #import "Source/common/SNTCommonEnums.h" #import "Source/common/SNTRuleIdentifiers.h" +#import "Source/common/SNTRule.h" @class MOLCodesignChecker; @class SNTCachedDecision; @@ -60,4 +61,13 @@ - (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath identifiers:(nonnull SNTRuleIdentifiers *)identifiers; + +/// +/// Updates a decision for a given file and agent configuration. +/// +/// Returns YES if the decision requires no futher processing NO otherwise. +- (BOOL) decision:(nonnull SNTCachedDecision *) cd + forRule:(nonnull SNTRule *) rule + withTransitiveRules:(BOOL) transitive; + @end diff --git a/Source/santad/SNTPolicyProcessor.m b/Source/santad/SNTPolicyProcessor.mm similarity index 66% rename from Source/santad/SNTPolicyProcessor.m rename to Source/santad/SNTPolicyProcessor.mm index 7b509cf50..53e4d687b 100644 --- a/Source/santad/SNTPolicyProcessor.m +++ b/Source/santad/SNTPolicyProcessor.mm @@ -27,6 +27,7 @@ #import "Source/common/SNTLogging.h" #import "Source/common/SNTRule.h" #import "Source/santad/DataLayer/SNTRuleTable.h" +#include "absl/container/flat_hash_map.h" @interface SNTPolicyProcessor () @property SNTRuleTable *ruleTable; @@ -44,6 +45,119 @@ - (instancetype)initWithRuleTable:(SNTRuleTable *)ruleTable { return self; } +// This method applies the rules to the cached decision object. +// +// It returns YES if the decision was made, NO if the decision was not made. +- (BOOL)decision:(SNTCachedDecision *)cd + forRule:(SNTRule *)rule + withTransitiveRules:(bool)enableTransitiveRules { + static const auto decisions = + absl::flat_hash_map, SNTEventState>{ + {{SNTRuleTypeCDHash, SNTRuleStateAllow}, SNTEventStateAllowCDHash}, + {{SNTRuleTypeCDHash, SNTRuleStateAllowCompiler}, SNTEventStateAllowCompiler}, + {{SNTRuleTypeCDHash, SNTRuleStateAllowTransitive}, SNTEventStateAllowTransitive}, + {{SNTRuleTypeCDHash, SNTRuleStateBlock}, SNTEventStateBlockCDHash}, + {{SNTRuleTypeCDHash, SNTRuleStateSilentBlock}, SNTEventStateBlockCDHash}, + {{SNTRuleTypeBinary, SNTRuleStateAllow}, SNTEventStateAllowBinary}, + {{SNTRuleTypeBinary, SNTRuleStateAllowTransitive}, SNTEventStateAllowTransitive}, + {{SNTRuleTypeBinary, SNTRuleStateAllowCompiler}, SNTEventStateAllowCompiler}, + {{SNTRuleTypeBinary, SNTRuleStateSilentBlock}, SNTEventStateBlockBinary}, + {{SNTRuleTypeBinary, SNTRuleStateBlock}, SNTEventStateBlockBinary}, + {{SNTRuleTypeSigningID, SNTRuleStateAllow}, SNTEventStateAllowSigningID}, + {{SNTRuleTypeSigningID, SNTRuleStateAllowCompiler}, SNTEventStateAllowCompiler}, + {{SNTRuleTypeSigningID, SNTRuleStateSilentBlock}, SNTEventStateBlockSigningID}, + {{SNTRuleTypeSigningID, SNTRuleStateBlock}, SNTEventStateBlockSigningID}, + {{SNTRuleTypeCertificate, SNTRuleStateAllow}, SNTEventStateAllowCertificate}, + {{SNTRuleTypeCertificate, SNTRuleStateSilentBlock}, SNTEventStateBlockCertificate}, + {{SNTRuleTypeCertificate, SNTRuleStateBlock}, SNTEventStateBlockCertificate}, + {{SNTRuleTypeTeamID, SNTRuleStateAllow}, SNTEventStateAllowTeamID}, + {{SNTRuleTypeTeamID, SNTRuleStateSilentBlock}, SNTEventStateBlockTeamID}, + {{SNTRuleTypeTeamID, SNTRuleStateBlock}, SNTEventStateBlockTeamID}, + }; + + auto iterator = decisions.find(std::pair{rule.type, rule.state}); + if (iterator != decisions.end()) { + cd.decision = iterator->second; + } else { + // If we have an invalid state combination then either we have stale data in + // the database or a programming error. We treat this as if the + // corresponding rule was not found. + LOGE(@"Invalid rule type/state combination %ld/%ld", rule.type, rule.state); + return NO; + } + + switch (rule.state) { + case SNTRuleStateSilentBlock: cd.silentBlock = YES; break; + case SNTRuleStateAllowCompiler: + if (!enableTransitiveRules) { + switch (rule.type) { + case SNTRuleTypeCDHash: cd.decision = SNTEventStateAllowCDHash; break; + case SNTRuleTypeBinary: cd.decision = SNTEventStateAllowBinary; break; + case SNTRuleTypeSigningID: cd.decision = SNTEventStateAllowSigningID; break; + default: + // Programming error. Something's marked as a compiler that shouldn't + // be. + LOGE(@"Invalid compiler rule type %ld", rule.type); + [NSException + raise:@"Invalid compiler rule type" + format:@"decision:forRule:withTransitiveRules: Unexpected compiler rule type: %ld", + rule.type]; + break; + } + } + break; + case SNTRuleStateAllowTransitive: + // If transitive rules are disabled, then we treat + // SNTRuleStateAllowTransitive rules as if a matching rule was not found + // and set the state to unknown. Otherwise the decision map will have already set + // the EventState to SNTEventStateAllowTransitive. + if (!enableTransitiveRules) { + cd.decision = SNTEventStateUnknown; + return NO; + } + break; + default: + // If its not one of the special cases above, we don't need to do anything. + break; + } + + // We know we have a match so apply the custom messages + cd.customMsg = rule.customMsg; + cd.customURL = rule.customURL; + + return YES; +} + +void updateCachedDecisionSigningInfo( + SNTCachedDecision *cd, MOLCodesignChecker *csInfo, + NSDictionary *_Nullable (^entitlementsFilterCallback)(NSDictionary *_Nullable entitlements)) { + cd.certSHA256 = csInfo.leafCertificate.SHA256; + cd.certCommonName = csInfo.leafCertificate.commonName; + cd.certChain = csInfo.certificates; + // Check if we need to get teamID from code signing. + if (!cd.teamID) { + cd.teamID = csInfo.teamID; + } + + // Ensure that if no teamID exists that the signing info confirms it is a + // platform binary. If not, remove the signingID. + if (!cd.teamID && cd.signingID) { + if (!csInfo.platformBinary) { + cd.signingID = nil; + } + } + + NSDictionary *entitlements = csInfo.entitlements; + + if (entitlementsFilterCallback) { + cd.entitlements = entitlementsFilterCallback(entitlements); + cd.entitlementsFiltered = (cd.entitlements.count != entitlements.count); + } else { + cd.entitlements = [entitlements sntDeepCopy]; + cd.entitlementsFiltered = NO; + } +} + - (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo cdhash:(nullable NSString *)cdhash fileSHA256:(nullable NSString *)fileSHA256 @@ -54,23 +168,27 @@ - (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileIn entitlementsFilterCallback: (NSDictionary *_Nullable (^_Nullable)( NSDictionary *_Nullable entitlements))entitlementsFilterCallback { - SNTCachedDecision *cd = [[SNTCachedDecision alloc] init]; - cd.cdhash = cdhash; - cd.sha256 = fileSHA256 ?: fileInfo.SHA256; - cd.teamID = teamID; - cd.signingID = signingID; - + // Check the hash before allocating a SNTCachedDecision. + NSString *fileHash = fileSHA256 ?: fileInfo.SHA256; SNTClientMode mode = [self.configurator clientMode]; - cd.decisionClientMode = mode; // If the binary is a critical system binary, don't check its signature. // The binary was validated at startup when the rule table was initialized. - SNTCachedDecision *systemCd = self.ruleTable.criticalSystemBinaries[cd.sha256]; + SNTCachedDecision *systemCd = self.ruleTable.criticalSystemBinaries[fileHash]; if (systemCd) { systemCd.decisionClientMode = mode; return systemCd; } + // Allocate a new cached decision for the execution. + SNTCachedDecision *cd = [[SNTCachedDecision alloc] init]; + cd.cdhash = cdhash; + cd.sha256 = fileHash; + cd.teamID = teamID; + cd.signingID = signingID; + cd.decisionClientMode = mode; + cd.quarantineURL = fileInfo.quarantineDataURL; + NSError *csInfoError; if (certificateSHA256.length) { cd.certSHA256 = certificateSHA256; @@ -87,31 +205,9 @@ - (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileIn cd.signingID = nil; cd.cdhash = nil; } else { - cd.certSHA256 = csInfo.leafCertificate.SHA256; - cd.certCommonName = csInfo.leafCertificate.commonName; - cd.certChain = csInfo.certificates; - cd.teamID = teamID ?: csInfo.teamID; - - // Ensure that if no teamID exists that the signing info confirms it is a - // platform binary. If not, remove the signingID. - if (!cd.teamID && cd.signingID) { - if (!csInfo.platformBinary) { - cd.signingID = nil; - } - } - - NSDictionary *entitlements = csInfo.entitlements; - - if (entitlementsFilterCallback) { - cd.entitlements = entitlementsFilterCallback(entitlements); - cd.entitlementsFiltered = (cd.entitlements.count == entitlements.count); - } else { - cd.entitlements = [entitlements sntDeepCopy]; - cd.entitlementsFiltered = NO; - } + updateCachedDecisionSigningInfo(cd, csInfo, entitlementsFilterCallback); } } - cd.quarantineURL = fileInfo.quarantineDataURL; // Do not evaluate TeamID/SigningID rules for dev-signed code based on the // assumption that orgs are generally more relaxed about dev signed cert @@ -133,116 +229,11 @@ - (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileIn .certificateSHA256 = cd.certSHA256, .teamID = cd.teamID}]; if (rule) { - switch (rule.type) { - case SNTRuleTypeCDHash: - switch (rule.state) { - case SNTRuleStateAllow: cd.decision = SNTEventStateAllowCDHash; return cd; - case SNTRuleStateAllowCompiler: - // If transitive rules are enabled, then SNTRuleStateAllowListCompiler rules - // become SNTEventStateAllowCompiler decisions. Otherwise we treat the rule as if - // it were SNTRuleStateAllowCDHash. - if ([self.configurator enableTransitiveRules]) { - cd.decision = SNTEventStateAllowCompiler; - } else { - cd.decision = SNTEventStateAllowCDHash; - } - return cd; - case SNTRuleStateSilentBlock: - cd.silentBlock = YES; - // intentional fallthrough - case SNTRuleStateBlock: - cd.customMsg = rule.customMsg; - cd.customURL = rule.customURL; - cd.decision = SNTEventStateBlockCDHash; - return cd; - default: break; - } - case SNTRuleTypeBinary: - switch (rule.state) { - case SNTRuleStateAllow: cd.decision = SNTEventStateAllowBinary; return cd; - case SNTRuleStateSilentBlock: cd.silentBlock = YES; - case SNTRuleStateBlock: - cd.customMsg = rule.customMsg; - cd.customURL = rule.customURL; - cd.decision = SNTEventStateBlockBinary; - return cd; - case SNTRuleStateAllowCompiler: - // If transitive rules are enabled, then SNTRuleStateAllowListCompiler rules - // become SNTEventStateAllowCompiler decisions. Otherwise we treat the rule as if - // it were SNTRuleStateAllow. - if ([self.configurator enableTransitiveRules]) { - cd.decision = SNTEventStateAllowCompiler; - } else { - cd.decision = SNTEventStateAllowBinary; - } - return cd; - case SNTRuleStateAllowTransitive: - // If transitive rules are enabled, then SNTRuleStateAllowTransitive - // rules become SNTEventStateAllowTransitive decisions. Otherwise, we treat the - // rule as if it were SNTRuleStateUnknown. - if ([self.configurator enableTransitiveRules]) { - cd.decision = SNTEventStateAllowTransitive; - return cd; - } else { - rule.state = SNTRuleStateUnknown; - } - default: break; - } - break; - case SNTRuleTypeSigningID: - switch (rule.state) { - case SNTRuleStateAllow: cd.decision = SNTEventStateAllowSigningID; return cd; - case SNTRuleStateAllowCompiler: - // If transitive rules are enabled, then SNTRuleStateAllowListCompiler rules - // become SNTEventStateAllowCompiler decisions. Otherwise we treat the rule as if - // it were SNTRuleStateAllowSigningID. - if ([self.configurator enableTransitiveRules]) { - cd.decision = SNTEventStateAllowCompiler; - } else { - cd.decision = SNTEventStateAllowSigningID; - } - return cd; - case SNTRuleStateSilentBlock: - cd.silentBlock = YES; - // intentional fallthrough - case SNTRuleStateBlock: - cd.customMsg = rule.customMsg; - cd.customURL = rule.customURL; - cd.decision = SNTEventStateBlockSigningID; - return cd; - default: break; - } - break; - case SNTRuleTypeCertificate: - switch (rule.state) { - case SNTRuleStateAllow: cd.decision = SNTEventStateAllowCertificate; return cd; - case SNTRuleStateSilentBlock: - cd.silentBlock = YES; - // intentional fallthrough - case SNTRuleStateBlock: - cd.customMsg = rule.customMsg; - cd.customURL = rule.customURL; - cd.decision = SNTEventStateBlockCertificate; - return cd; - default: break; - } - break; - case SNTRuleTypeTeamID: - switch (rule.state) { - case SNTRuleStateAllow: cd.decision = SNTEventStateAllowTeamID; return cd; - case SNTRuleStateSilentBlock: - cd.silentBlock = YES; - // intentional fallthrough - case SNTRuleStateBlock: - cd.customMsg = rule.customMsg; - cd.customURL = rule.customURL; - cd.decision = SNTEventStateBlockTeamID; - return cd; - default: break; - } - break; - - default: break; + // If we have a rule match we don't need to process any further. + if ([self decision:cd + forRule:rule + withTransitiveRules:self.configurator.enableTransitiveRules]) { + return cd; } } diff --git a/Source/santad/SNTPolicyProcessorTest.m b/Source/santad/SNTPolicyProcessorTest.m new file mode 100644 index 000000000..d27ead485 --- /dev/null +++ b/Source/santad/SNTPolicyProcessorTest.m @@ -0,0 +1,564 @@ +/// Copyright 2024 Google Inc. All rights reserved. +/// +/// 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 +#import "Source/santad/SNTPolicyProcessor.h" + +#import +#import "Source/common/SNTCachedDecision.h" +#import "Source/common/SNTConfigurator.h" +#import "Source/common/SNTRule.h" + +#import "Source/santad/SNTPolicyProcessor.h" + +@interface SNTPolicyProcessorTest : XCTestCase +@property SNTPolicyProcessor *processor; +@end + +@implementation SNTPolicyProcessorTest +- (void)setUp { + _processor = [[SNTPolicyProcessor alloc] init]; +} + +- (void)testRule:(SNTRule *)rule + transitiveRules:(BOOL)transitiveRules + final:(BOOL)final + matches:(BOOL)matches + silent:(BOOL)silent + expectedDecision:(SNTEventState)decision { + SNTCachedDecision *cd = [[SNTCachedDecision alloc] init]; + if (matches) { + switch (rule.type) { + case SNTRuleTypeBinary: cd.sha256 = rule.identifier; break; + case SNTRuleTypeCertificate: cd.certSHA256 = rule.identifier; break; + case SNTRuleTypeCDHash: cd.cdhash = rule.identifier; break; + default: break; + } + } else { + switch (rule.type) { + case SNTRuleTypeBinary: + cd.sha256 = @"2334567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + break; + case SNTRuleTypeCertificate: + cd.certSHA256 = @"2234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + break; + case SNTRuleTypeCDHash: cd.cdhash = @"b023fbe5361a5bbd793dc3889556e93f41ec9bb8"; break; + default: break; + } + } + BOOL decisionIsFinal = [self.processor decision:cd + forRule:rule + withTransitiveRules:transitiveRules]; + XCTAssertEqual(cd.decision, decision); + XCTAssertEqual(decisionIsFinal, final); + XCTAssertEqual(cd.silentBlock, silent); +} + +- (void)testDecisionForBlockByCDHashRuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"CDHASH", + @"identifier" : @"a023fbe5361a5bbd793dc3889556e93f41ec9bb8", + @"policy" : @"BLOCKLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateBlockCDHash]; + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateBlockCDHash]; +} + +- (void)testDecisionForSilentBlockByCDHashRuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"CDHASH", + @"identifier" : @"a023fbe5361a5bbd793dc3889556e93f41ec9bb8", + @"policy" : @"SILENT_BLOCKLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:YES + expectedDecision:SNTEventStateBlockCDHash]; + // Ensure that nothing changes when disabling transitive rules. + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:YES + expectedDecision:SNTEventStateBlockCDHash]; +} + +- (void)testDecisionForAllowbyCDHashRuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"CDHASH", + @"identifier" : @"a023fbe5361a5bbd793dc3889556e93f41ec9bb8", + @"policy" : @"ALLOWLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowCDHash]; + // Ensure that nothing changes when disabling transitive rules. + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowCDHash]; +} + +- (void)testDecisionForBlockBySHA256RuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"BINARY", + @"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + @"policy" : @"BLOCKLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateBlockBinary]; + // Ensure that nothing changes when disabling transitive rules. + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateBlockBinary]; +} + +- (void)testDecisionForSilenBlockBySHA256RuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"BINARY", + @"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + @"policy" : @"SILENT_BLOCKLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:YES + expectedDecision:SNTEventStateBlockBinary]; + // Ensure that nothing changes when disabling transitive rules. + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:YES + expectedDecision:SNTEventStateBlockBinary]; +} + +- (void)testDecisionForAllowBySHA256RuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"BINARY", + @"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + @"policy" : @"ALLOWLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowBinary]; + // Ensure that nothing changes when disabling transitive rules. + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowBinary]; +} + +- (void)testDecisionForSigningIDBlockRuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"SIGNINGID", + @"identifier" : @"ABCDEFGHIJ:ABCDEFGHIJ", + @"policy" : @"BLOCKLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateBlockSigningID]; + // Ensure that nothing changes when disabling transitive rules. + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateBlockSigningID]; +} + +// Signing ID rules +- (void)testDecisionForSigningIDSilentBlockRuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"SIGNINGID", + @"identifier" : @"TEAMID1234:ABCDEFGHIJ", + @"policy" : @"SILENT_BLOCKLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:YES + expectedDecision:SNTEventStateBlockSigningID]; + // Ensure that nothing changes when disabling transitive rules. + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:YES + expectedDecision:SNTEventStateBlockSigningID]; +} + +- (void)testDecisionForSigningIDAllowRuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"SIGNINGID", + @"identifier" : @"TEAMID1234:ABCDEFGHIJ", + @"policy" : @"ALLOWLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowSigningID]; + // Ensure that nothing changes when disabling transitive rules. + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowSigningID]; +} + +// Certificate rules +- (void)testDecisionForCertificateBlockRuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"CERTIFICATE", + @"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + @"policy" : @"BLOCKLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateBlockCertificate]; + // Ensure that nothing changes when disabling transitive rules. + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateBlockCertificate]; +} + +- (void)testDecisionForCertificateSilentBlockRuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"CERTIFICATE", + @"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + @"policy" : @"SILENT_BLOCKLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:YES + expectedDecision:SNTEventStateBlockCertificate]; + // Ensure that nothing changes when disabling transitive rules. + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:YES + expectedDecision:SNTEventStateBlockCertificate]; +} + +- (void)testDecisionForCertificateAllowRuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"CERTIFICATE", + @"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + @"policy" : @"ALLOWLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowCertificate]; + // Ensure that nothing changes when disabling transitive rules. + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowCertificate]; +} + +// Team ID rules +- (void)testDecisionForTeamIDBlockRuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"TEAMID", + @"identifier" : @"TEAMID1234", + @"policy" : @"BLOCKLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateBlockTeamID]; + // Ensure that nothing changes when disabling transitive rules. + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateBlockTeamID]; +} + +- (void)testDecisionForTeamIDSilentBlockRuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"TEAMID", + @"identifier" : @"TEAMID1234", + @"policy" : @"SILENT_BLOCKLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:YES + expectedDecision:SNTEventStateBlockTeamID]; + // Ensure that nothing changes when disabling transitive rules. + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:YES + expectedDecision:SNTEventStateBlockTeamID]; +} + +- (void)testDecisionForTeamIDAllowRuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"TEAMID", + @"identifier" : @"TEAMID1234", + @"policy" : @"ALLOWLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowTeamID]; + // Ensure that nothing changes when disabling transitive rules. + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowTeamID]; +} + +// Compiler rules +// CDHash +- (void)testDecisionForCDHashCompilerRuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"CDHASH", + @"identifier" : @"a023fbe5361a5bbd793dc3889556e93f41ec9bb8", + @"policy" : @"ALLOWLIST_COMPILER" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowCompiler]; + // Ensure disabling transitive rules results in a binary allow + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowCDHash]; +} + +// SHA256 +- (void)testDecisionForSHA256CompilerRuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"BINARY", + @"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + @"policy" : @"ALLOWLIST_COMPILER" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowCompiler]; + // Ensure disabling transitive rules results in a binary allow + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowBinary]; +} + +// SigningID +- (void)testDecisionForSigningIDCompilerRuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"SIGNINGID", + @"identifier" : @"TEAMID1234:ABCDEFGHIJ", + @"policy" : @"ALLOWLIST_COMPILER" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowCompiler]; + // Ensure disabling transitive rules results in a Signing ID allow + [self testRule:rule + transitiveRules:NO + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowSigningID]; +} + +// Transitive allowlist rules +- (void)testDecisionForTransitiveAllowlistRuleMatches { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"BINARY", + @"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + @"policy" : @"ALLOWLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + + rule.state = SNTRuleStateAllowTransitive; + + [self testRule:rule + transitiveRules:YES + final:YES + matches:YES + silent:NO + expectedDecision:SNTEventStateAllowTransitive]; + // Ensure that a transitive allowlist rule results in an + // SNTEventStateUnknown if transitive rules are disabled. + [self testRule:rule + transitiveRules:NO + final:NO + matches:YES + silent:NO + expectedDecision:SNTEventStateUnknown]; +} + +- (void)testEnsureANonMatchingRuleResultsInUnknown { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"BINARY", + @"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + @"policy" : @"ALLOWLIST" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + + rule.state = 88888; // Set to an invalid state + + [self testRule:rule + transitiveRules:YES + final:NO + matches:NO + silent:NO + expectedDecision:SNTEventStateUnknown]; + + [self testRule:rule + transitiveRules:NO + final:NO + matches:YES + silent:NO + expectedDecision:SNTEventStateUnknown]; +} + +- (void)testEnsureCustomURLAndMessageAreSet { + SNTRule *rule = [[SNTRule alloc] initWithDictionary:@{ + @"rule_type" : @"BINARY", + @"identifier" : @"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + @"policy" : @"ALLOWLIST", + @"custom_msg" : @"Custom Message", + @"custom_url" : @"https://example.com" + }]; + + XCTAssertNotNil(rule, "invalid test rule dictionary"); + + SNTCachedDecision *cd = [[SNTCachedDecision alloc] init]; + cd.sha256 = rule.identifier; + + [self.processor decision:cd forRule:rule withTransitiveRules:YES]; + + XCTAssertEqualObjects(cd.customMsg, @"Custom Message"); + XCTAssertEqualObjects(cd.customURL, @"https://example.com"); +} + +@end \ No newline at end of file