From ce2777ae94835b8a2ecd28230a2c14c1329b18cf Mon Sep 17 00:00:00 2001 From: Matt W <436037+mlw@users.noreply.github.com> Date: Wed, 3 Jan 2024 09:52:14 -0500 Subject: [PATCH] Fix `santactl rule --check` (#1262) * Fix santactl rule check to only strictly show rule info * Reorganized to make more testable, added tests --- Source/santactl/BUILD | 12 ++ Source/santactl/Commands/SNTCommandRule.h | 20 +++ Source/santactl/Commands/SNTCommandRule.m | 140 +++++++++++------- .../santactl/Commands/SNTCommandRuleTest.mm | 96 ++++++++++++ 4 files changed, 211 insertions(+), 57 deletions(-) create mode 100644 Source/santactl/Commands/SNTCommandRule.h create mode 100644 Source/santactl/Commands/SNTCommandRuleTest.mm diff --git a/Source/santactl/BUILD b/Source/santactl/BUILD index e41ccc68a..231f253ef 100644 --- a/Source/santactl/BUILD +++ b/Source/santactl/BUILD @@ -41,6 +41,7 @@ objc_library( "Commands/SNTCommandFileInfo.m", "Commands/SNTCommandMetrics.h", "Commands/SNTCommandMetrics.m", + "Commands/SNTCommandRule.h", "Commands/SNTCommandRule.m", "Commands/SNTCommandStatus.m", "Commands/SNTCommandSync.m", @@ -147,11 +148,22 @@ santa_unit_test( ], ) +santa_unit_test( + name = "SNTCommandRuleTest", + srcs = ["Commands/SNTCommandRuleTest.mm"], + deps = [ + ":santactl_lib", + "//Source/common:SNTCommonEnums", + "//Source/common:SNTRule", + ], +) + test_suite( name = "unit_tests", tests = [ ":SNTCommandFileInfoTest", ":SNTCommandMetricsTest", + ":SNTCommandRuleTest", ], visibility = ["//:santa_package_group"], ) diff --git a/Source/santactl/Commands/SNTCommandRule.h b/Source/santactl/Commands/SNTCommandRule.h new file mode 100644 index 000000000..3e33ac894 --- /dev/null +++ b/Source/santactl/Commands/SNTCommandRule.h @@ -0,0 +1,20 @@ +/// Copyright 2024 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 + +#import "Source/santactl/SNTCommand.h" + +@interface SNTCommandRule : SNTCommand +@end diff --git a/Source/santactl/Commands/SNTCommandRule.m b/Source/santactl/Commands/SNTCommandRule.m index 87c166389..e270e701d 100644 --- a/Source/santactl/Commands/SNTCommandRule.m +++ b/Source/santactl/Commands/SNTCommandRule.m @@ -23,12 +23,10 @@ #import "Source/common/SNTLogging.h" #import "Source/common/SNTRule.h" #import "Source/common/SNTXPCControlInterface.h" +#import "Source/santactl/Commands/SNTCommandRule.h" #import "Source/santactl/SNTCommand.h" #import "Source/santactl/SNTCommandController.h" -@interface SNTCommandRule : SNTCommand -@end - @implementation SNTCommandRule REGISTER_COMMAND_NAME(@"rule") @@ -286,72 +284,100 @@ - (void)runWithArguments:(NSArray *)arguments { }]; } +// IMPORTANT: This method makes no attempt to validate whether or not the data +// in a rule is valid. It merely constructs a string with the given data. +// E.g., TeamID compiler rules are not currently supproted, but if a test rule +// is provided with that state, an appropriate string will be returned. ++ (NSString *)stringifyRule:(SNTRule *)rule withColor:(BOOL)colorize { + NSMutableString *output; + // Rule state is saved as eventState for output colorization down below + SNTEventState eventState = SNTEventStateUnknown; + + switch (rule.state) { + case SNTRuleStateUnknown: + output = [@"No rule exists with the given parameters" mutableCopy]; + break; + case SNTRuleStateAllow: OS_FALLTHROUGH; + case SNTRuleStateAllowCompiler: OS_FALLTHROUGH; + case SNTRuleStateAllowTransitive: + output = [@"Allowed" mutableCopy]; + eventState = SNTEventStateAllow; + break; + case SNTRuleStateBlock: OS_FALLTHROUGH; + case SNTRuleStateSilentBlock: + output = [@"Blocked" mutableCopy]; + eventState = SNTEventStateBlock; + break; + case SNTRuleStateRemove: OS_FALLTHROUGH; + default: + output = [NSMutableString stringWithFormat:@"Unexpected rule state: %ld", rule.state]; + break; + } + + if (rule.state == SNTRuleStateUnknown) { + // No more output to append + return output; + } + + [output appendString:@" ("]; + + switch (rule.type) { + case SNTRuleTypeUnknown: [output appendString:@"Unknown"]; break; + case SNTRuleTypeBinary: [output appendString:@"Binary"]; break; + case SNTRuleTypeSigningID: [output appendString:@"SigningID"]; break; + case SNTRuleTypeCertificate: [output appendString:@"Certificate"]; break; + case SNTRuleTypeTeamID: [output appendString:@"TeamID"]; break; + default: + output = [NSMutableString stringWithFormat:@"Unexpected rule type: %ld", rule.type]; + break; + } + + // Add additional attributes + switch (rule.state) { + case SNTRuleStateAllowCompiler: [output appendString:@", Compiler"]; break; + case SNTRuleStateAllowTransitive: [output appendString:@", Transitive"]; break; + case SNTRuleStateSilentBlock: [output appendString:@", Silent"]; break; + default: break; + } + + [output appendString:@")"]; + + // Colorize + if (colorize) { + if ((SNTEventStateAllow & eventState)) { + [output insertString:@"\033[32m" atIndex:0]; + [output appendString:@"\033[0m"]; + } else if ((SNTEventStateBlock & eventState)) { + [output insertString:@"\033[31m" atIndex:0]; + [output appendString:@"\033[0m"]; + } else { + [output insertString:@"\033[33m" atIndex:0]; + [output appendString:@"\033[0m"]; + } + } + + if (rule.state == SNTRuleStateAllowTransitive) { + NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:rule.timestamp]; + [output appendString:[NSString stringWithFormat:@"\nlast access date: %@", [date description]]]; + } + return output; +} + - (void)printStateOfRule:(SNTRule *)rule daemonConnection:(MOLXPCConnection *)daemonConn { id rop = [daemonConn synchronousRemoteObjectProxy]; NSString *fileSHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil; NSString *certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil; NSString *teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil; NSString *signingID = (rule.type == SNTRuleTypeSigningID) ? rule.identifier : nil; - __block NSMutableString *output; - [rop decisionForFilePath:nil - fileSHA256:fileSHA256 - certificateSHA256:certificateSHA256 - teamID:teamID - signingID:signingID - reply:^(SNTEventState s) { - output = - (SNTEventStateAllow & s) ? @"Allowed".mutableCopy : @"Blocked".mutableCopy; - switch (s) { - case SNTEventStateAllowUnknown: - case SNTEventStateBlockUnknown: [output appendString:@" (Unknown)"]; break; - case SNTEventStateAllowBinary: - case SNTEventStateBlockBinary: [output appendString:@" (Binary)"]; break; - case SNTEventStateAllowCertificate: - case SNTEventStateBlockCertificate: - [output appendString:@" (Certificate)"]; - break; - case SNTEventStateAllowScope: - case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break; - case SNTEventStateAllowCompiler: - [output appendString:@" (Compiler)"]; - break; - case SNTEventStateAllowTransitive: - [output appendString:@" (Transitive)"]; - break; - case SNTEventStateAllowTeamID: - case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break; - case SNTEventStateAllowSigningID: - case SNTEventStateBlockSigningID: - [output appendString:@" (SigningID)"]; - break; - default: output = @"None".mutableCopy; break; - } - if (isatty(STDOUT_FILENO)) { - if ((SNTEventStateAllow & s)) { - [output insertString:@"\033[32m" atIndex:0]; - [output appendString:@"\033[0m"]; - } else if ((SNTEventStateBlock & s)) { - [output insertString:@"\033[31m" atIndex:0]; - [output appendString:@"\033[0m"]; - } else { - [output insertString:@"\033[33m" atIndex:0]; - [output appendString:@"\033[0m"]; - } - } - }]; + __block NSString *output; [rop databaseRuleForBinarySHA256:fileSHA256 certificateSHA256:certificateSHA256 teamID:teamID signingID:signingID reply:^(SNTRule *r) { - if (r.state == SNTRuleStateAllowTransitive) { - NSDate *date = - [NSDate dateWithTimeIntervalSinceReferenceDate:r.timestamp]; - [output appendString:[NSString - stringWithFormat:@"\nlast access date: %@", - [date description]]]; - } + output = [SNTCommandRule stringifyRule:r + withColor:(isatty(STDOUT_FILENO) == 1)]; }]; printf("%s\n", output.UTF8String); diff --git a/Source/santactl/Commands/SNTCommandRuleTest.mm b/Source/santactl/Commands/SNTCommandRuleTest.mm new file mode 100644 index 000000000..2d8140e1b --- /dev/null +++ b/Source/santactl/Commands/SNTCommandRuleTest.mm @@ -0,0 +1,96 @@ +/// Copyright 2024 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 +#import + +#include +#include + +#import "Source/common/SNTRule.h" +#import "Source/santactl/Commands/SNTCommandRule.h" + +@interface SNTCommandRule (Testing) ++ (NSString *)stringifyRule:(SNTRule *)rule withColor:(BOOL)colorize; +@end + +@interface SNTRule () +@property(readwrite) NSUInteger timestamp; +@end + +@interface SNTCommandRuleTest : XCTestCase +@end + +@implementation SNTCommandRuleTest + +- (void)testStringifyRule { + std::map, NSString *> ruleCheckToString = { + {{SNTRuleTypeUnknown, SNTRuleStateUnknown}, @"No rule exists with the given parameters"}, + {{SNTRuleTypeUnknown, SNTRuleStateAllow}, @"Allowed (Unknown)"}, + {{SNTRuleTypeUnknown, SNTRuleStateBlock}, @"Blocked (Unknown)"}, + {{SNTRuleTypeUnknown, SNTRuleStateSilentBlock}, @"Blocked (Unknown, Silent)"}, + {{SNTRuleTypeUnknown, SNTRuleStateRemove}, @"Unexpected rule state: 4 (Unknown)"}, + {{SNTRuleTypeUnknown, SNTRuleStateAllowCompiler}, @"Allowed (Unknown, Compiler)"}, + {{SNTRuleTypeUnknown, SNTRuleStateAllowTransitive}, + @"Allowed (Unknown, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"}, + + {{SNTRuleTypeBinary, SNTRuleStateUnknown}, @"No rule exists with the given parameters"}, + {{SNTRuleTypeBinary, SNTRuleStateAllow}, @"Allowed (Binary)"}, + {{SNTRuleTypeBinary, SNTRuleStateBlock}, @"Blocked (Binary)"}, + {{SNTRuleTypeBinary, SNTRuleStateSilentBlock}, @"Blocked (Binary, Silent)"}, + {{SNTRuleTypeBinary, SNTRuleStateRemove}, @"Unexpected rule state: 4 (Binary)"}, + {{SNTRuleTypeBinary, SNTRuleStateAllowCompiler}, @"Allowed (Binary, Compiler)"}, + {{SNTRuleTypeBinary, SNTRuleStateAllowTransitive}, + @"Allowed (Binary, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"}, + + {{SNTRuleTypeSigningID, SNTRuleStateUnknown}, @"No rule exists with the given parameters"}, + {{SNTRuleTypeSigningID, SNTRuleStateAllow}, @"Allowed (SigningID)"}, + {{SNTRuleTypeSigningID, SNTRuleStateBlock}, @"Blocked (SigningID)"}, + {{SNTRuleTypeSigningID, SNTRuleStateSilentBlock}, @"Blocked (SigningID, Silent)"}, + {{SNTRuleTypeSigningID, SNTRuleStateRemove}, @"Unexpected rule state: 4 (SigningID)"}, + {{SNTRuleTypeSigningID, SNTRuleStateAllowCompiler}, @"Allowed (SigningID, Compiler)"}, + {{SNTRuleTypeSigningID, SNTRuleStateAllowTransitive}, + @"Allowed (SigningID, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"}, + + {{SNTRuleTypeCertificate, SNTRuleStateUnknown}, @"No rule exists with the given parameters"}, + {{SNTRuleTypeCertificate, SNTRuleStateAllow}, @"Allowed (Certificate)"}, + {{SNTRuleTypeCertificate, SNTRuleStateBlock}, @"Blocked (Certificate)"}, + {{SNTRuleTypeCertificate, SNTRuleStateSilentBlock}, @"Blocked (Certificate, Silent)"}, + {{SNTRuleTypeCertificate, SNTRuleStateRemove}, @"Unexpected rule state: 4 (Certificate)"}, + {{SNTRuleTypeCertificate, SNTRuleStateAllowCompiler}, @"Allowed (Certificate, Compiler)"}, + {{SNTRuleTypeCertificate, SNTRuleStateAllowTransitive}, + @"Allowed (Certificate, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"}, + + {{SNTRuleTypeTeamID, SNTRuleStateUnknown}, @"No rule exists with the given parameters"}, + {{SNTRuleTypeTeamID, SNTRuleStateAllow}, @"Allowed (TeamID)"}, + {{SNTRuleTypeTeamID, SNTRuleStateBlock}, @"Blocked (TeamID)"}, + {{SNTRuleTypeTeamID, SNTRuleStateSilentBlock}, @"Blocked (TeamID, Silent)"}, + {{SNTRuleTypeTeamID, SNTRuleStateRemove}, @"Unexpected rule state: 4 (TeamID)"}, + {{SNTRuleTypeTeamID, SNTRuleStateAllowCompiler}, @"Allowed (TeamID, Compiler)"}, + {{SNTRuleTypeTeamID, SNTRuleStateAllowTransitive}, + @"Allowed (TeamID, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"}, + }; + + SNTRule *rule = [[SNTRule alloc] init]; + rule.timestamp = 700000000; // time interval since reference date + + for (const auto &[typeAndState, want] : ruleCheckToString) { + rule.type = typeAndState.first; + rule.state = typeAndState.second; + + NSString *got = [SNTCommandRule stringifyRule:rule withColor:NO]; + XCTAssertEqualObjects(got, want); + } +} +@end