diff --git a/Source/common/BUILD b/Source/common/BUILD index 2d77a48f..34f38c52 100644 --- a/Source/common/BUILD +++ b/Source/common/BUILD @@ -246,6 +246,7 @@ objc_library( objc_library( name = "SNTCommonEnums", + module_name = "santa_common_SNTCommonEnums", textual_hdrs = ["SNTCommonEnums.h"], ) diff --git a/Source/common/SNTCommonEnums.h b/Source/common/SNTCommonEnums.h index 390de01d..0403377f 100644 --- a/Source/common/SNTCommonEnums.h +++ b/Source/common/SNTCommonEnums.h @@ -63,13 +63,15 @@ typedef NS_ENUM(NSInteger, SNTRuleState) { SNTRuleStateAllowCompiler = 5, SNTRuleStateAllowTransitive = 6, + SNTRuleStateAllowLocalBinary = 7, + SNTRuleStateAllowLocalSigningID = 8, }; typedef NS_ENUM(NSInteger, SNTClientMode) { SNTClientModeUnknown, - SNTClientModeMonitor = 1, SNTClientModeLockdown = 2, + SNTClientModeStandalone = 3, }; typedef NS_ENUM(uint64_t, SNTEventState) { @@ -98,6 +100,8 @@ typedef NS_ENUM(uint64_t, SNTEventState) { SNTEventStateAllowTeamID = 1ULL << 47, SNTEventStateAllowSigningID = 1ULL << 48, SNTEventStateAllowCDHash = 1ULL << 49, + SNTEventStateAllowLocalBinary = 1ULL << 50, + SNTEventStateAllowLocalSigningID = 1ULL << 51, // Block and Allow masks SNTEventStateBlock = 0xFFFFFFULL << 16, diff --git a/Source/common/SNTConfigurator.h b/Source/common/SNTConfigurator.h index 06d80dc6..e39f8bf1 100644 --- a/Source/common/SNTConfigurator.h +++ b/Source/common/SNTConfigurator.h @@ -399,6 +399,12 @@ /// @property(nullable, readonly, nonatomic) NSString *modeNotificationLockdown; +/// +/// The notification text to display when the client goes into STANDALONE mode. +/// Defaults to "Switching into Standalone mode" +/// +@property(nullable, readonly, nonatomic) NSString *modeNotificationStandalone; + /// /// If this is set to true, the UI will use different fonts on April 1st, May 4th and October 31st. /// diff --git a/Source/common/SNTConfigurator.m b/Source/common/SNTConfigurator.m index d8f7cf7d..24ece2cb 100644 --- a/Source/common/SNTConfigurator.m +++ b/Source/common/SNTConfigurator.m @@ -106,6 +106,7 @@ @implementation SNTConfigurator static NSString *const kModeNotificationMonitor = @"ModeNotificationMonitor"; static NSString *const kModeNotificationLockdown = @"ModeNotificationLockdown"; +static NSString *const kModeNotificationStandalone = @"ModeNotificationStandalone"; static NSString *const kFunFontsOnSpecificDays = @"FunFontsOnSpecificDays"; static NSString *const kEnablePageZeroProtectionKey = @"EnablePageZeroProtection"; @@ -238,6 +239,7 @@ - (instancetype)initWithSyncStateFile:(NSString *)syncStateFilePath kRemountUSBBlockMessage : string, kModeNotificationMonitor : string, kModeNotificationLockdown : string, + kModeNotificationStandalone : string, kFunFontsOnSpecificDays : number, kStaticRules : array, kSyncBaseURLKey : string, @@ -633,12 +635,12 @@ + (NSSet *)keyPathsForValuesAffectingTelemetry { - (SNTClientMode)clientMode { SNTClientMode cm = [self.syncState[kClientModeKey] longLongValue]; - if (cm == SNTClientModeMonitor || cm == SNTClientModeLockdown) { + if (cm == SNTClientModeMonitor || cm == SNTClientModeLockdown || cm == SNTClientModeStandalone) { return cm; } cm = [self.configState[kClientModeKey] longLongValue]; - if (cm == SNTClientModeMonitor || cm == SNTClientModeLockdown) { + if (cm == SNTClientModeMonitor || cm == SNTClientModeLockdown || cm == SNTClientModeStandalone) { return cm; } @@ -646,14 +648,17 @@ - (SNTClientMode)clientMode { } - (void)setSyncServerClientMode:(SNTClientMode)newMode { - if (newMode == SNTClientModeMonitor || newMode == SNTClientModeLockdown) { + if (newMode == SNTClientModeMonitor || newMode == SNTClientModeLockdown || + newMode == SNTClientModeStandalone) { [self updateSyncStateForKey:kClientModeKey value:@(newMode)]; } } - (BOOL)failClosed { NSNumber *n = self.configState[kFailClosedKey]; - return [n boolValue] && self.clientMode == SNTClientModeLockdown; + BOOL runningInLockdownClientMode = + self.clientMode == SNTClientModeLockdown || self.clientMode == SNTClientModeStandalone; + return [n boolValue] && runningInLockdownClientMode; } - (BOOL)enableTransitiveRules { @@ -842,6 +847,10 @@ - (NSString *)modeNotificationLockdown { return self.configState[kModeNotificationLockdown]; } +- (NSString *)modeNotificationStandalone { + return self.configState[kModeNotificationStandalone]; +} + - (BOOL)funFontsOnSpecificDays { return [self.configState[kFunFontsOnSpecificDays] boolValue]; } diff --git a/Source/common/SNTRule.m b/Source/common/SNTRule.m index 0481c244..3c45c746 100644 --- a/Source/common/SNTRule.m +++ b/Source/common/SNTRule.m @@ -1,4 +1,5 @@ /// Copyright 2015 Google Inc. All rights reserved. +/// 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. @@ -166,6 +167,10 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict { } else if ([policyString isEqual:kRulePolicyAllowlistCompiler] || [policyString isEqual:kRulePolicyAllowlistCompilerDeprecated]) { state = SNTRuleStateAllowCompiler; + } else if ([policyString isEqual:kRulePolicyAllowlistLocalBinary]) { + state = SNTRuleStateAllowLocalBinary; + } else if ([policyString isEqual:kRulePolicyAllowlistLocalSigningID]) { + state = SNTRuleStateAllowLocalSigningID; } else if ([policyString isEqual:kRulePolicyBlocklist] || [policyString isEqual:kRulePolicyBlocklistDeprecated]) { state = SNTRuleStateBlock; @@ -254,6 +259,8 @@ - (NSString *)ruleStateToPolicyString:(SNTRuleState)state { case SNTRuleStateSilentBlock: return kRulePolicySilentBlocklist; case SNTRuleStateRemove: return kRulePolicyRemove; case SNTRuleStateAllowTransitive: return @"AllowTransitive"; + case SNTRuleStateAllowLocalBinary: return kRulePolicyAllowlistLocalBinary; + case SNTRuleStateAllowLocalSigningID: return kRulePolicyAllowlistLocalSigningID; // This should never be hit. But is here for completion. default: return @"Unknown"; } diff --git a/Source/common/SNTSyncConstants.h b/Source/common/SNTSyncConstants.h index d9326db5..bcf1b0bf 100644 --- a/Source/common/SNTSyncConstants.h +++ b/Source/common/SNTSyncConstants.h @@ -116,6 +116,8 @@ extern NSString *const kRuleSHA256; extern NSString *const kRuleIdentifier; extern NSString *const kRulePolicy; extern NSString *const kRulePolicyAllowlist; +extern NSString *const kRulePolicyAllowlistLocalBinary; +extern NSString *const kRulePolicyAllowlistLocalSigningID; extern NSString *const kRulePolicyAllowlistDeprecated; extern NSString *const kRulePolicyAllowlistCompiler; extern NSString *const kRulePolicyAllowlistCompilerDeprecated; diff --git a/Source/common/SNTSyncConstants.m b/Source/common/SNTSyncConstants.m index 534348b0..78b6cb69 100644 --- a/Source/common/SNTSyncConstants.m +++ b/Source/common/SNTSyncConstants.m @@ -117,6 +117,8 @@ NSString *const kRuleIdentifier = @"identifier"; NSString *const kRulePolicy = @"policy"; NSString *const kRulePolicyAllowlist = @"ALLOWLIST"; +NSString *const kRulePolicyAllowlistLocalBinary = @"ALLOWLIST_LOCAL_BINARY"; +NSString *const kRulePolicyAllowlistLocalSigningID = @"ALLOWLIST_LOCAL_SIGNINGID"; NSString *const kRulePolicyAllowlistDeprecated = @"WHITELIST"; NSString *const kRulePolicyAllowlistCompiler = @"ALLOWLIST_COMPILER"; NSString *const kRulePolicyAllowlistCompilerDeprecated = @"WHITELIST_COMPILER"; diff --git a/Source/common/SNTXPCNotifierInterface.h b/Source/common/SNTXPCNotifierInterface.h index 5ec8da00..8e922dbf 100644 --- a/Source/common/SNTXPCNotifierInterface.h +++ b/Source/common/SNTXPCNotifierInterface.h @@ -1,4 +1,5 @@ /// Copyright 2015 Google Inc. All rights reserved. +/// 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. @@ -25,7 +26,8 @@ @protocol SNTNotifierXPC - (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message - andCustomURL:(NSString *)url; + customURL:(NSString *)url + andReply:(void (^)(BOOL authenticated))reply; - (void)postUSBBlockNotification:(SNTDeviceEvent *)event; - (void)postFileAccessBlockNotification:(SNTFileAccessEvent *)event customMessage:(NSString *)message diff --git a/Source/common/santa.proto b/Source/common/santa.proto index 66141a27..8579faa9 100644 --- a/Source/common/santa.proto +++ b/Source/common/santa.proto @@ -298,6 +298,7 @@ message Execution { MODE_UNKNOWN = 0; MODE_LOCKDOWN = 1; MODE_MONITOR = 2; + MODE_STANDALONE = 3; } optional Mode mode = 11; diff --git a/Source/gui/BUILD b/Source/gui/BUILD index ffdf3af6..dc8878f8 100644 --- a/Source/gui/BUILD +++ b/Source/gui/BUILD @@ -29,6 +29,7 @@ swift_library( deps = [ ":SNTMessageView", "//Source/common:SNTBlockMessage_SantaGUI", + "//Source/common:SNTCommonEnums", "//Source/common:SNTConfigurator", "//Source/common:SNTStoredEvent", ], @@ -84,6 +85,7 @@ objc_library( "SNTNotificationManager.h", ], sdk_frameworks = [ + "LocalAuthentication", "IOKit", "SecurityInterface", "SystemExtensions", diff --git a/Source/gui/Resources/de.lproj/Localizable.strings b/Source/gui/Resources/de.lproj/Localizable.strings index fabd8da1..8a8c4fb5 100644 --- a/Source/gui/Resources/de.lproj/Localizable.strings +++ b/Source/gui/Resources/de.lproj/Localizable.strings @@ -1,9 +1,18 @@ /* The default message to show the user when access to a file is blocked */ "Access to a file has been denied" = "Der Zugriff auf eine Datei wurde verweigert"; +/* No comment provided by engineer. */ +"Approve" = "Genehmigen"; + /* No comment provided by engineer. */ "Application" = "Applikation"; +/* No comment provided by engineer. */ +"authorize execution of " = "genehmigen die Ausführung von "; + +/* No comment provided by engineer. */ +"authorize execution of the application " = "Die Ausführung der Anwendung autorisieren "; + /* No comment provided by engineer. */ "Bundle Hash" = "Bundle-Hash"; diff --git a/Source/gui/Resources/en.lproj/Localizable.strings b/Source/gui/Resources/en.lproj/Localizable.strings index 7b7d6ac3..433a1c42 100644 --- a/Source/gui/Resources/en.lproj/Localizable.strings +++ b/Source/gui/Resources/en.lproj/Localizable.strings @@ -4,6 +4,16 @@ /* No comment provided by engineer. */ "Application" = "Application"; +/* Default text for Approve */ +"Approve" = "Approve"; + +/* File path + Signing ID */ +"authorize execution of " = "authorize execution of "; + +/* Bundle name */ +"authorize execution of the application " = "authorize execution of the application "; + /* No comment provided by engineer. */ "Bundle Hash" = "Bundle Hash"; diff --git a/Source/gui/Resources/ru.lproj/Localizable.strings b/Source/gui/Resources/ru.lproj/Localizable.strings index bea51b0e..29f02253 100644 --- a/Source/gui/Resources/ru.lproj/Localizable.strings +++ b/Source/gui/Resources/ru.lproj/Localizable.strings @@ -1,9 +1,18 @@ /* The default message to show the user when access to a file is blocked */ "Access to a file has been denied" = "Доступ к файлу был запрещен"; +/* No comment provided by engineer. */ +"Approve" = "Утвердить"; + /* No comment provided by engineer. */ "Application" = "Приложение"; +/* No comment provided by engineer. */ +"authorize execution of " = "разрешить исполнение "; + +/* No comment provided by engineer. */ +"authorize execution of the application " = "разрешить выполнение заявки "; + /* No comment provided by engineer. */ "Bundle Hash" = "Бандл-хэш"; diff --git a/Source/gui/Resources/uk.lproj/Localizable.strings b/Source/gui/Resources/uk.lproj/Localizable.strings index cc4d168a..e4163d3a 100644 --- a/Source/gui/Resources/uk.lproj/Localizable.strings +++ b/Source/gui/Resources/uk.lproj/Localizable.strings @@ -1,6 +1,15 @@ +/* No comment provided by engineer. */ +"Approve" = "Затвердити"; + /* No comment provided by engineer. */ "Application" = "Заявка"; +/* No Comment provided by engineer. */ +"authorize execution of " = "дозволити виконання "; + +/* No Comment provided by engineer. */ +"authorize execution of the application " = "дозволити виконання заявки "; + /* No comment provided by engineer. */ "Bundle Hash" = "Пакетний хеш"; diff --git a/Source/gui/SNTBinaryMessageWindowController.h b/Source/gui/SNTBinaryMessageWindowController.h index 94ac7209..7bb1429c 100644 --- a/Source/gui/SNTBinaryMessageWindowController.h +++ b/Source/gui/SNTBinaryMessageWindowController.h @@ -27,7 +27,8 @@ - (instancetype)initWithEvent:(SNTStoredEvent *)event customMsg:(NSString *)message - customURL:(NSString *)url; + customURL:(NSString *)url + reply:(void (^)(BOOL authenticated))replyBlock; - (void)updateBlockNotification:(SNTStoredEvent *)event withBundleHash:(NSString *)bundleHash; @@ -49,6 +50,11 @@ /// @property(readonly) SNTStoredEvent *event; +/// +/// The reply block to call when the user has made a decision in standalone +/// mode. +@property(readonly, nonatomic) void (^replyBlock)(BOOL authenticated); + /// /// The root progress object. Child nodes are vended to santad to report on work being done. /// diff --git a/Source/gui/SNTBinaryMessageWindowController.m b/Source/gui/SNTBinaryMessageWindowController.m index 15168a87..2a4d6af1 100644 --- a/Source/gui/SNTBinaryMessageWindowController.m +++ b/Source/gui/SNTBinaryMessageWindowController.m @@ -17,8 +17,10 @@ #import "Source/gui/SNTBinaryMessageWindowView-Swift.h" #include +#import #import #import +#include #import "Source/common/CertificateHelpers.h" #import "Source/common/SNTBlockMessage.h" @@ -39,12 +41,14 @@ @implementation SNTBinaryMessageWindowController - (instancetype)initWithEvent:(SNTStoredEvent *)event customMsg:(NSString *)message - customURL:(NSString *)url { + customURL:(NSString *)url + reply:(void (^)(BOOL))replyBlock { self = [super init]; if (self) { _event = event; _customMessage = message; _customURL = url; + _replyBlock = replyBlock; _progress = [NSProgress discreteProgressWithTotalUnitCount:1]; [_progress addObserver:self forKeyPath:@"fractionCompleted" @@ -97,7 +101,8 @@ - (void)showWindow:(id)sender { bundleProgress:self.bundleProgress uiStateCallback:^(NSTimeInterval preventNotificationsPeriod) { self.silenceFutureNotificationsPeriod = preventNotificationsPeriod; - }]; + } + replyCallback:self.replyBlock]; self.window.delegate = self; diff --git a/Source/gui/SNTBinaryMessageWindowView.swift b/Source/gui/SNTBinaryMessageWindowView.swift index 15bdfa7b..43d8c63e 100644 --- a/Source/gui/SNTBinaryMessageWindowView.swift +++ b/Source/gui/SNTBinaryMessageWindowView.swift @@ -16,6 +16,7 @@ import SwiftUI import santa_common_SNTBlockMessage import santa_common_SNTConfigurator +import santa_common_SNTCommonEnums import santa_common_SNTStoredEvent import santa_gui_SNTMessageView @@ -34,7 +35,8 @@ import santa_gui_SNTMessageView customMsg: NSString?, customURL: NSString?, bundleProgress: SNTBundleProgress, - uiStateCallback: ((TimeInterval) -> Void)? + uiStateCallback: ((TimeInterval) -> Void)?, + replyCallback: ((Bool) -> Void)? ) -> NSViewController { return NSHostingController( rootView: SNTBinaryMessageWindowView( @@ -43,7 +45,8 @@ import santa_gui_SNTMessageView customMsg: customMsg, customURL: customURL, bundleProgress: bundleProgress, - uiStateCallback: uiStateCallback + uiStateCallback: uiStateCallback, + replyCallback: replyCallback ) .fixedSize() ) @@ -238,10 +241,8 @@ struct SNTBinaryMessageEventView: View { .keyboardShortcut("d", modifiers: .command) .help("⌘ d") } - Spacer() } - } } @@ -252,6 +253,7 @@ struct SNTBinaryMessageWindowView: View { let customURL: NSString? @StateObject var bundleProgress: SNTBundleProgress let uiStateCallback: ((TimeInterval) -> Void)? + let replyCallback: ((Bool) -> Void)? @Environment(\.openURL) var openURL @@ -266,41 +268,68 @@ struct SNTBinaryMessageWindowView: View { ) { SNTBinaryMessageEventView(e: event!, customURL: customURL, bundleProgress: bundleProgress) - VStack(spacing: 15.0) { - SNTNotificationSilenceView( - silence: $preventFutureNotifications, - period: $preventFutureNotificationPeriod - ) + SNTNotificationSilenceView(silence: $preventFutureNotifications, period: $preventFutureNotificationPeriod) - if event?.needsBundleHash ?? false && !bundleProgress.isFinished { - if bundleProgress.fractionCompleted == 0.0 { - ProgressView { - Text(bundleProgress.label) - }.progressViewStyle(.linear) - } else { - ProgressView(value: bundleProgress.fractionCompleted) { - Text(bundleProgress.label) - } + if event?.needsBundleHash ?? false && !bundleProgress.isFinished { + if bundleProgress.fractionCompleted == 0.0 { + ProgressView() { + Text(bundleProgress.label) + }.progressViewStyle(.linear) + } else { + ProgressView(value: bundleProgress.fractionCompleted) { + Text(bundleProgress.label) } } + } - HStack(spacing: 15.0) { - if !(c.eventDetailURL?.isEmpty ?? false) - && !(event?.needsBundleHash ?? false && !bundleProgress.isFinished) - { - OpenEventButton(customText: c.eventDetailText, action: openButton) + // Display the standalone error message to the user if one is provided. + if c.clientMode == .standalone { + let (canAuthz, err) = CanAuthorizeWithTouchID() + if !canAuthz { + if let errMsg = err { + Text(errMsg.localizedDescription).foregroundColor(.red) } - DismissButton( - customText: c.dismissText, - silence: preventFutureNotifications, - action: dismissButton - ) } } + HStack(spacing: 15.0) { + if !(c.eventDetailURL?.isEmpty ?? false) + && !(event?.needsBundleHash ?? false && !bundleProgress.isFinished) && c.clientMode != .standalone + { + OpenEventButton(customText: c.eventDetailText, action: openButton) + } else if addStandaloneButton() { + StandaloneButton(action: standAloneButton) + } + + DismissButton( + customText: c.dismissText, + silence: preventFutureNotifications, + action: dismissButton + ) + } Spacer() + }.fixedSize() + } + + func addStandaloneButton() -> Bool { + var shouldDisplay = c.clientMode == .standalone + + let (canAuthz, _) = CanAuthorizeWithTouchID() + if !canAuthz { + shouldDisplay = false + } + + let blockedUnknownEvent = SNTEventState.blockUnknown; + + // Only display the standalone button if the event is for a block that fell + // was the result of a fall through. + if let decision = event?.decision { + if decision != blockedUnknownEvent { + shouldDisplay = false + } } - .fixedSize() + + return shouldDisplay } func openButton() { @@ -312,11 +341,69 @@ struct SNTBinaryMessageWindowView: View { } } - let url = SNTBlockMessage.eventDetailURL(for: event, customURL: customURL as String?) + if let callback = replyCallback { + callback(false) + } + window?.close() - if let url = url { + + let detailsURL = SNTBlockMessage.eventDetailURL(for: event, customURL: customURL as String?) + + if let url = detailsURL { openURL(url) } + + } + + // This button is only shown when the standalone mode is enabled in place of + // the "Open Event" button. + func standAloneButton() { + guard let e = self.event else { + if let cb = self.replyCallback { + cb(false) + } + return + } + + let bundleName = e.fileBundleName ?? "" + let filePath = e.filePath ?? "" + let signingID = e.signingID ?? "" + + var msg = "authorize execution" + + if !bundleName.isEmpty { + msg = NSLocalizedString( + "authorize execution of the application " + bundleName, + comment: "Bundle name" + ) + } else if !signingID.isEmpty { + msg = NSLocalizedString( + "authorize execution of " + signingID, + comment: "Signing ID" + ) + } else if !filePath.isEmpty { + msg = NSLocalizedString( + "authorize execution of " + filePath, + comment: "File path" + ) + } + + // Force unwrap the callback because it should always be set and is a + // programming error if it isn't. + // + // Note: this may prevent other replyBlocks from being run, but should only + // crash the GUI process meaning policy decisions will still be enforced. + let callback = self.replyCallback!; + + AuthorizeViaTouchID( + reason: msg, + replyBlock: { success in + callback(success) + DispatchQueue.main.sync { + window?.close() + } + } + ) } func dismissButton() { @@ -327,6 +414,11 @@ struct SNTBinaryMessageWindowView: View { callback(0) } } + + // Close the window after responding to the block. + if let callback = replyCallback { + callback(false) + } window?.close() } } diff --git a/Source/gui/SNTFileAccessMessageWindowView.swift b/Source/gui/SNTFileAccessMessageWindowView.swift index d960e667..ec6ad79f 100644 --- a/Source/gui/SNTFileAccessMessageWindowView.swift +++ b/Source/gui/SNTFileAccessMessageWindowView.swift @@ -193,7 +193,6 @@ struct SNTFileAccessMessageWindowView: View { } DismissButton(silence: preventFutureNotifications, action: dismissButton) } - Spacer() } } diff --git a/Source/gui/SNTMessageView.swift b/Source/gui/SNTMessageView.swift index f0c049d3..fcee6060 100644 --- a/Source/gui/SNTMessageView.swift +++ b/Source/gui/SNTMessageView.swift @@ -1,4 +1,5 @@ import SwiftUI +import LocalAuthentication import santa_common_SNTConfigurator @@ -149,6 +150,45 @@ public func OpenEventButton(customText: String? = nil, action: @escaping () -> V .help("⌘ Return") } +public func AuthorizeViaTouchID(reason: String, replyBlock: @escaping (Bool) -> Void) { + LAContext().evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, error in + if error != nil { + replyBlock(false) + } else { + replyBlock(success) + } + } +} + +// CanAuthorizeWithTouchID checks if TouchID is available on the current device +// and returns an error if it is not. +public func CanAuthorizeWithTouchID() -> (Bool, NSError?) { + let context = LAContext() + var error: NSError? + + if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { + return (true, nil) + } else { + return (false, error) + } +} + +// StandaloneButton is only used in Standalone mode. It's a replacement for the +// Open event button. +// +// It is intended to be used for all approvals in the future if in standalone +// mode. +public func StandaloneButton(action: @escaping () -> Void) -> some View { + Button( + action: action, + label: { + Text(NSLocalizedString("Approve", comment: "Default text for Approve")).frame(maxWidth: 200.0) + } + ) + .keyboardShortcut(.return, modifiers: .command) + .help("⌘ Return") +} + public func DismissButton( customText: String? = nil, silence: Bool?, diff --git a/Source/gui/SNTNotificationManager.m b/Source/gui/SNTNotificationManager.m index 62dd638e..7d3e3630 100644 --- a/Source/gui/SNTNotificationManager.m +++ b/Source/gui/SNTNotificationManager.m @@ -1,4 +1,5 @@ /// Copyright 2015 Google Inc. All rights reserved. +/// 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. @@ -14,6 +15,7 @@ #import "Source/gui/SNTNotificationManager.h" #include +#include "Source/common/SNTCommonEnums.h" #import #import @@ -110,7 +112,14 @@ - (void)queueMessage:(SNTMessageWindowController *)pendingMsg { if ([SNTConfigurator configurator].enableSilentMode) return; dispatch_async(dispatch_get_main_queue(), ^{ - if ([self notificationAlreadyQueued:pendingMsg]) return; + if ([self notificationAlreadyQueued:pendingMsg]) { + // Make sure we clear the reply block so we don't leak memory. + if ([pendingMsg isKindOfClass:[SNTBinaryMessageWindowController class]]) { + SNTBinaryMessageWindowController *bmwc = (SNTBinaryMessageWindowController *)pendingMsg; + bmwc.replyBlock(NO); + } + return; + } // See if this message has been user-silenced. NSString *messageHash = [pendingMsg messageHash]; @@ -305,6 +314,16 @@ - (void)postClientModeNotification:(SNTClientMode)clientmode { content.body = [SNTBlockMessage stringFromHTML:customMsg]; break; } + case SNTClientModeStandalone: { + content.body = @"Switching into Standalone mode"; + NSString *customMsg = [[SNTConfigurator configurator] modeNotificationStandalone]; + if (!customMsg) break; + // If a custom message is added but as an empty string, disable notifications. + if (!customMsg.length) return; + + content.body = [SNTBlockMessage stringFromHTML:customMsg]; + break; + } default: return; } @@ -336,14 +355,18 @@ - (void)postRuleSyncNotificationWithCustomMessage:(NSString *)message { - (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message - andCustomURL:(NSString *)url { + customURL:(NSString *)url + andReply:(void (^)(BOOL))replyBlock { if (!event) { LOGI(@"Error: Missing event object in message received from daemon!"); return; } SNTBinaryMessageWindowController *pendingMsg = - [[SNTBinaryMessageWindowController alloc] initWithEvent:event customMsg:message customURL:url]; + [[SNTBinaryMessageWindowController alloc] initWithEvent:event + customMsg:message + customURL:url + reply:replyBlock]; [self queueMessage:pendingMsg]; } diff --git a/Source/gui/SNTNotificationManagerTest.m b/Source/gui/SNTNotificationManagerTest.m index 7b128935..acf46ab0 100644 --- a/Source/gui/SNTNotificationManagerTest.m +++ b/Source/gui/SNTNotificationManagerTest.m @@ -58,7 +58,11 @@ - (void)testPostBlockNotificationSendsDistributedNotification { id dncMock = OCMClassMock([NSDistributedNotificationCenter class]); OCMStub([dncMock defaultCenter]).andReturn(dncMock); - [sut postBlockNotification:ev withCustomMessage:@"" andCustomURL:@""]; + [sut postBlockNotification:ev + withCustomMessage:@"" + customURL:@"" + andReply:^(BOOL authenticated){ + }]; OCMVerify([dncMock postNotificationName:@"com.northpolesec.santa.notification.blockedeexecution" object:@"com.northpolesec.santa" diff --git a/Source/santactl/Commands/SNTCommandFileInfo.m b/Source/santactl/Commands/SNTCommandFileInfo.m index 36b15a98..dddfed09 100644 --- a/Source/santactl/Commands/SNTCommandFileInfo.m +++ b/Source/santactl/Commands/SNTCommandFileInfo.m @@ -416,12 +416,14 @@ - (SNTAttributeBlock)rule { case SNTEventStateAllowUnknown: case SNTEventStateBlockUnknown: [output appendString:@" (Unknown)"]; break; case SNTEventStateAllowBinary: + case SNTEventStateAllowLocalBinary: case SNTEventStateBlockBinary: [output appendString:@" (Binary)"]; break; case SNTEventStateAllowCertificate: case SNTEventStateBlockCertificate: [output appendString:@" (Certificate)"]; break; case SNTEventStateAllowTeamID: case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break; case SNTEventStateAllowSigningID: + case SNTEventStateAllowLocalSigningID: case SNTEventStateBlockSigningID: [output appendString:@" (SigningID)"]; break; case SNTEventStateAllowCDHash: case SNTEventStateBlockCDHash: [output appendString:@" (CDHash)"]; break; diff --git a/Source/santactl/Commands/SNTCommandStatus.m b/Source/santactl/Commands/SNTCommandStatus.m index dfb54a7f..0eca2ae2 100644 --- a/Source/santactl/Commands/SNTCommandStatus.m +++ b/Source/santactl/Commands/SNTCommandStatus.m @@ -1,4 +1,5 @@ /// Copyright 2015-2022 Google Inc. All rights reserved. +/// 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. @@ -66,6 +67,7 @@ - (void)runWithArguments:(NSArray *)arguments { switch (cm) { case SNTClientModeMonitor: clientMode = @"Monitor"; break; case SNTClientModeLockdown: clientMode = @"Lockdown"; break; + case SNTClientModeStandalone: clientMode = @"Standalone"; break; default: clientMode = [NSString stringWithFormat:@"Unknown (%ld)", cm]; break; } }]; diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm b/Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm index 54ff6f77..471e6218 100644 --- a/Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm +++ b/Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm @@ -26,6 +26,7 @@ #include #include #include +#include "Source/common/SNTCommonEnums.h" #include #include @@ -95,6 +96,7 @@ static inline SanitizableString FilePath(const es_file_t *file) { switch (mode) { case SNTClientModeMonitor: return "M"; case SNTClientModeLockdown: return "L"; + case SNTClientModeStandalone: return "S"; default: return "U"; } } diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm b/Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm index a37bcbf0..a4dd4971 100644 --- a/Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm +++ b/Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm @@ -954,6 +954,7 @@ - (void)testGetModeString { std::map modeToString = { {SNTClientModeMonitor, "M"}, {SNTClientModeLockdown, "L"}, + {SNTClientModeStandalone, "S"}, {(SNTClientMode)123, "U"}, }; diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm b/Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm index d5c0564f..1d717c89 100644 --- a/Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm +++ b/Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm @@ -30,6 +30,7 @@ #include #import "Source/common/SNTCachedDecision.h" +#include "Source/common/SNTCommonEnums.h" #include "Source/common/SNTLogging.h" #import "Source/common/SNTStoredEvent.h" #import "Source/common/String.h" @@ -332,6 +333,7 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info, switch (mode) { case SNTClientModeMonitor: return ::pbv1::Execution::MODE_MONITOR; case SNTClientModeLockdown: return ::pbv1::Execution::MODE_LOCKDOWN; + case SNTClientModeStandalone: return ::pbv1::Execution::MODE_STANDALONE; case SNTClientModeUnknown: return ::pbv1::Execution::MODE_UNKNOWN; default: return ::pbv1::Execution::MODE_UNKNOWN; } diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm b/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm index ce76cf2a..f8de74fe 100644 --- a/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm +++ b/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm @@ -531,6 +531,7 @@ - (void)testGetModeEnum { {SNTClientModeUnknown, ::pbv1::Execution::MODE_UNKNOWN}, {SNTClientModeMonitor, ::pbv1::Execution::MODE_MONITOR}, {SNTClientModeLockdown, ::pbv1::Execution::MODE_LOCKDOWN}, + {SNTClientModeStandalone, ::pbv1::Execution::MODE_STANDALONE}, {(SNTClientMode)123, ::pbv1::Execution::MODE_UNKNOWN}, }; diff --git a/Source/santad/SNTApplicationCoreMetrics.mm b/Source/santad/SNTApplicationCoreMetrics.mm index cda4eb31..fda510c6 100644 --- a/Source/santad/SNTApplicationCoreMetrics.mm +++ b/Source/santad/SNTApplicationCoreMetrics.mm @@ -38,6 +38,7 @@ static void RegisterModeMetric(SNTMetricSet *metricSet) { switch (config.clientMode) { case SNTClientModeLockdown: [mode set:@"lockdown" forFieldValues:@[]]; break; + case SNTClientModeStandalone: [mode set:@"standalone" forFieldValues:@[]]; break; case SNTClientModeMonitor: [mode set:@"monitor" forFieldValues:@[]]; break; default: // Should never be reached. diff --git a/Source/santad/SNTExecutionController.h b/Source/santad/SNTExecutionController.h index c3083663..54712d1d 100644 --- a/Source/santad/SNTExecutionController.h +++ b/Source/santad/SNTExecutionController.h @@ -20,12 +20,14 @@ const static NSString *kBlockBinary = @"BlockBinary"; const static NSString *kAllowBinary = @"AllowBinary"; +const static NSString *kAllowLocalBinary = @"AllowLocalBinary"; const static NSString *kBlockCertificate = @"BlockCertificate"; const static NSString *kAllowCertificate = @"AllowCertificate"; const static NSString *kBlockTeamID = @"BlockTeamID"; const static NSString *kAllowTeamID = @"AllowTeamID"; const static NSString *kBlockSigningID = @"BlockSigningID"; const static NSString *kAllowSigningID = @"AllowSigningID"; +const static NSString *kAllowLocalSigningID = @"AllowLocalSigningID"; const static NSString *kBlockCDHash = @"BlockCDHash"; const static NSString *kAllowCDHash = @"AllowCDHash"; const static NSString *kBlockScope = @"BlockScope"; diff --git a/Source/santad/SNTExecutionController.mm b/Source/santad/SNTExecutionController.mm index 5fb23b8e..9f67b385 100644 --- a/Source/santad/SNTExecutionController.mm +++ b/Source/santad/SNTExecutionController.mm @@ -1,5 +1,5 @@ - /// Copyright 2015-2022 Google Inc. All rights reserved. +/// 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. @@ -156,6 +156,7 @@ - (void)incrementEventCounters:(SNTEventState)eventType { switch (eventType) { case SNTEventStateBlockBinary: eventTypeStr = kBlockBinary; break; case SNTEventStateAllowBinary: eventTypeStr = kAllowBinary; break; + case SNTEventStateAllowLocalBinary: eventTypeStr = kAllowLocalBinary; break; case SNTEventStateBlockCertificate: eventTypeStr = kBlockCertificate; break; case SNTEventStateAllowCertificate: eventTypeStr = kAllowCertificate; break; case SNTEventStateBlockTeamID: eventTypeStr = kBlockTeamID; break; @@ -404,8 +405,29 @@ - (void)validateExecEvent:(const Message &)esMsg postAction:(bool (^)(SNTAction) self->_ttyWriter->Write(targetProc, msg); } + void (^replyBlock)(BOOL) = ^void(BOOL authenticated) { + }; + + // Only allow a user in standalone mode to override a block if an + // explicit block rule is not set when using a sync service. + if (config.clientMode == SNTClientModeStandalone && + se.decision == SNTEventStateBlockUnknown) { + replyBlock = ^void(BOOL authenticated) { + LOGD(@"User responded to block event for %@ with authenticated: %d", se.filePath, + authenticated); + if (authenticated) { + // Create a rule for the binary that was allowed by the user in + // standalone mode and notify the sync service + [self createRuleForStandaloneModeEvent:se]; + } + }; + } + // Let the user know what happened in the GUI. - [self.notifierQueue addEvent:se withCustomMessage:cd.customMsg andCustomURL:cd.customURL]; + [self.notifierQueue addEvent:se + withCustomMessage:cd.customMsg + customURL:cd.customURL + andReply:replyBlock]; } } } @@ -480,4 +502,38 @@ - (void)loggedInUsers:(NSArray **)users sessions:(NSArray **)sessions { *sessions = [loggedInHosts copy]; } +// Creates a rule for the binary that was allowed by the user in standalone mode. +- (void)createRuleForStandaloneModeEvent:(SNTStoredEvent *)se { + SNTRuleType ruleType = SNTRuleTypeSigningID; + NSString *ruleIdentifier = se.signingID; + SNTRuleState newRuleState = SNTRuleStateAllowLocalSigningID; + + // Check here to see if the binary is validly signed if not + // then use a hash rule instead of a signing ID + if (se.signingChain.count == 0) { + LOGD(@"No certificate chain found for %@", se.filePath); + ruleType = SNTRuleTypeBinary; + ruleIdentifier = se.fileSHA256; + newRuleState = SNTRuleStateAllowLocalBinary; + } + + NSString *commentStr = [NSString stringWithFormat:@"%@", se.filePath]; + + // Add rule to allow binary same as santactl rule. + SNTRule *newRule = [[SNTRule alloc] initWithIdentifier:ruleIdentifier + state:newRuleState + type:ruleType + customMsg:@"" + timestamp:[[NSDate now] timeIntervalSince1970] + comment:commentStr]; + NSError *err; + [self.ruleTable addRules:@[ newRule ] ruleCleanup:SNTRuleCleanupNone error:&err]; + if (err) { + LOGE(@"Failed to add rule in standalone mode for %@: %@", se.filePath, + err.localizedDescription); + } + + // TODO: Notify the sync service of the new rule. +} + @end diff --git a/Source/santad/SNTNotificationQueue.h b/Source/santad/SNTNotificationQueue.h index e3a8babe..8618e02a 100644 --- a/Source/santad/SNTNotificationQueue.h +++ b/Source/santad/SNTNotificationQueue.h @@ -1,4 +1,5 @@ /// Copyright 2016 Google Inc. All rights reserved. +/// 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. @@ -23,6 +24,7 @@ - (void)addEvent:(SNTStoredEvent *)event withCustomMessage:(NSString *)message - andCustomURL:(NSString *)url; + customURL:(NSString *)url + andReply:(void (^)(BOOL authenticated))reply; @end diff --git a/Source/santad/SNTNotificationQueue.m b/Source/santad/SNTNotificationQueue.m index 54d307ac..e900d400 100644 --- a/Source/santad/SNTNotificationQueue.m +++ b/Source/santad/SNTNotificationQueue.m @@ -38,10 +38,15 @@ - (instancetype)init { - (void)addEvent:(SNTStoredEvent *)event withCustomMessage:(NSString *)message - andCustomURL:(NSString *)url { - if (!event) return; + customURL:(NSString *)url + andReply:(void (^)(BOOL authenticated))reply { + if (!event) { + if (reply) reply(NO); + return; + } if (self.pendingNotifications.count > kMaximumNotifications) { LOGI(@"Pending GUI notification count is over %d, dropping.", kMaximumNotifications); + if (reply) reply(NO); return; } @@ -52,6 +57,15 @@ - (void)addEvent:(SNTStoredEvent *)event if (url) { d[@"url"] = url; } + + if (reply) { + // Copy the block to the heap so it can be called later. + // + // This is necessary because the block is allocated on the stack in the + // Execution controller which goes out of scope. + d[@"reply"] = [reply copy]; + } + @synchronized(self.pendingNotifications) { [self.pendingNotifications addObject:d]; } @@ -67,7 +81,8 @@ - (void)flushQueue { for (NSDictionary *d in self.pendingNotifications) { [rop postBlockNotification:d[@"event"] withCustomMessage:d[@"message"] - andCustomURL:d[@"url"]]; + customURL:d[@"url"] + andReply:d[@"reply"]]; [postedNotifications addObject:d]; } [self.pendingNotifications removeObjectsInArray:postedNotifications]; diff --git a/Source/santad/SNTPolicyProcessor.mm b/Source/santad/SNTPolicyProcessor.mm index 1084586d..47105256 100644 --- a/Source/santad/SNTPolicyProcessor.mm +++ b/Source/santad/SNTPolicyProcessor.mm @@ -65,11 +65,13 @@ - (BOOL)decision:(SNTCachedDecision *)cd {{SNTRuleTypeCDHash, SNTRuleStateBlock}, SNTEventStateBlockCDHash}, {{SNTRuleTypeCDHash, SNTRuleStateSilentBlock}, SNTEventStateBlockCDHash}, {{SNTRuleTypeBinary, SNTRuleStateAllow}, SNTEventStateAllowBinary}, + {{SNTRuleTypeBinary, SNTRuleStateAllowLocalBinary}, SNTEventStateAllowLocalBinary}, {{SNTRuleTypeBinary, SNTRuleStateAllowTransitive}, SNTEventStateAllowTransitive}, {{SNTRuleTypeBinary, SNTRuleStateAllowCompiler}, SNTEventStateAllowCompiler}, {{SNTRuleTypeBinary, SNTRuleStateSilentBlock}, SNTEventStateBlockBinary}, {{SNTRuleTypeBinary, SNTRuleStateBlock}, SNTEventStateBlockBinary}, {{SNTRuleTypeSigningID, SNTRuleStateAllow}, SNTEventStateAllowSigningID}, + {{SNTRuleTypeSigningID, SNTRuleStateAllowLocalSigningID}, SNTEventStateAllowLocalSigningID}, {{SNTRuleTypeSigningID, SNTRuleStateAllowCompiler}, SNTEventStateAllowCompiler}, {{SNTRuleTypeSigningID, SNTRuleStateSilentBlock}, SNTEventStateBlockSigningID}, {{SNTRuleTypeSigningID, SNTRuleStateBlock}, SNTEventStateBlockSigningID}, @@ -285,6 +287,7 @@ static void UpdateCachedDecisionSigningInfo( switch (mode) { case SNTClientModeMonitor: cd.decision = SNTEventStateAllowUnknown; return cd; + case SNTClientModeStandalone: [[fallthrough]]; case SNTClientModeLockdown: cd.decision = SNTEventStateBlockUnknown; return cd; default: cd.decision = SNTEventStateBlockUnknown; return cd; } diff --git a/Source/santad/Santad.mm b/Source/santad/Santad.mm index 850b1538..9a0c094e 100644 --- a/Source/santad/Santad.mm +++ b/Source/santad/Santad.mm @@ -179,6 +179,11 @@ void SantadMain(std::shared_ptr esapi, std::shared_ptrFlushCache(FlushCacheMode::kAllCaches, FlushCacheReason::kClientModeChanged); break; + case SNTClientModeStandalone: + LOGI(@"Changed client mode to Standalone, flushing cache."); + auth_result_cache->FlushCache(FlushCacheMode::kAllCaches, + FlushCacheReason::kClientModeChanged); + break; case SNTClientModeMonitor: LOGI(@"Changed client mode to Monitor."); break; default: LOGW(@"Changed client mode to unknown value."); break; } diff --git a/Source/santad/SantadTest.mm b/Source/santad/SantadTest.mm index 81a76d43..2b5cf3c6 100644 --- a/Source/santad/SantadTest.mm +++ b/Source/santad/SantadTest.mm @@ -236,6 +236,15 @@ - (void)testBinaryWithSHA256BlockRuleIsBlockedInLockdownMode { }]; } +- (void)testBinaryWithSHA256BlockRuleIsBlockedInStandaloneMode { + [self checkBinaryExecution:@"badbinary" + wantResult:ES_AUTH_RESULT_DENY + clientMode:SNTClientModeStandalone + cdValidator:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateBlockBinary; + }]; +} + - (void)testBinaryWithSHA256BlockRuleIsBlockedInMonitorMode { [self checkBinaryExecution:@"badbinary" wantResult:ES_AUTH_RESULT_DENY @@ -254,6 +263,15 @@ - (void)testBinaryWithSHA256AllowRuleIsNotBlockedInLockdownMode { }]; } +- (void)testBinaryWithSHA256AllowRuleIsNotBlockedInStandaloneMode { + [self checkBinaryExecution:@"goodbinary" + wantResult:ES_AUTH_RESULT_ALLOW + clientMode:SNTClientModeStandalone + cdValidator:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateAllowBinary; + }]; +} + - (void)testBinaryWithSHA256AllowRuleIsNotBlockedInMonitorMode { [self checkBinaryExecution:@"goodbinary" wantResult:ES_AUTH_RESULT_ALLOW @@ -272,6 +290,15 @@ - (void)testBinaryWithCertificateAllowRuleIsNotBlockedInLockdownMode { }]; } +- (void)testBinaryWithCertificateAllowRuleIsNotBlockedInStandaloneMode { + [self checkBinaryExecution:@"goodcert" + wantResult:ES_AUTH_RESULT_ALLOW + clientMode:SNTClientModeStandalone + cdValidator:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateAllowCertificate; + }]; +} + - (void)testBinaryWithCertificateAllowRuleIsNotBlockedInMonitorMode { [self checkBinaryExecution:@"goodcert" wantResult:ES_AUTH_RESULT_ALLOW @@ -290,6 +317,15 @@ - (void)testBinaryWithCertificateBlockRuleIsBlockedInLockdownMode { }]; } +- (void)testBinaryWithCertificateBlockRuleIsBlockedInStandaloneMode { + [self checkBinaryExecution:@"badcert" + wantResult:ES_AUTH_RESULT_DENY + clientMode:SNTClientModeStandalone + cdValidator:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateBlockCertificate; + }]; +} + - (void)testBinaryWithCertificateBlockRuleIsBlockedInMonitorMode { [self checkBinaryExecution:@"badcert" wantResult:ES_AUTH_RESULT_DENY @@ -312,6 +348,19 @@ - (void)testBinaryWithTeamIDAllowRuleAndNoSigningIDMatchIsAllowedInLockdownMode }]; } +- (void)testBinaryWithTeamIDAllowRuleAndNoSigningIDMatchIsAllowedInStandaloneMode { + [self checkBinaryExecution:@"allowed_teamid" + wantResult:ES_AUTH_RESULT_ALLOW + clientMode:SNTClientModeStandalone + cdValidator:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateAllowTeamID; + } + messageSetup:^(es_message_t *msg) { + msg->event.exec.target->team_id = MakeESStringToken(kAllowedTeamID); + msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID); + }]; +} + - (void)testBinaryWithTeamIDAllowRuleAndNoSigningIDMatchIsAllowedInMonitorMode { [self checkBinaryExecution:@"allowed_teamid" wantResult:ES_AUTH_RESULT_ALLOW @@ -338,6 +387,19 @@ - (void)testBinaryWithTeamIDBlockRuleAndNoSigningIDMatchIsBlockedInLockdownMode }]; } +- (void)testBinaryWithTeamIDBlockRuleAndNoSigningIDMatchIsBlockedInStandaloneMode { + [self checkBinaryExecution:@"banned_teamid" + wantResult:ES_AUTH_RESULT_DENY + clientMode:SNTClientModeStandalone + cdValidator:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateBlockTeamID; + } + messageSetup:^(es_message_t *msg) { + msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID); + msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID); + }]; +} + - (void)testBinaryWithTeamIDBlockRuleAndNoSigningIDMatchIsBlockedInMonitorMode { [self checkBinaryExecution:@"banned_teamid" wantResult:ES_AUTH_RESULT_DENY @@ -364,6 +426,19 @@ - (void)testBinaryWithSigningIDBlockRuleIsBlockedInLockdownMode { }]; } +- (void)testBinaryWithSigningIDBlockRuleIsBlockedInStandaloneMode { + [self checkBinaryExecution:@"banned_signingid" + wantResult:ES_AUTH_RESULT_DENY + clientMode:SNTClientModeStandalone + cdValidator:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateBlockSigningID; + } + messageSetup:^(es_message_t *msg) { + msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID); + msg->event.exec.target->signing_id = MakeESStringToken(kBlockedSigningID); + }]; +} + - (void)testBinaryWithSigningIDBlockRuleIsBlockedInMonitorMode { [self checkBinaryExecution:@"banned_signingid" wantResult:ES_AUTH_RESULT_DENY @@ -403,6 +478,19 @@ - (void)testBinaryWithSigningIDAllowRuleIsAllowedInLockdownMode { }]; } +- (void)testBinaryWithSigningIDAllowRuleIsAllowedInStandaloneMode { + [self checkBinaryExecution:@"allowed_signingid" + wantResult:ES_AUTH_RESULT_ALLOW + clientMode:SNTClientModeMonitor + cdValidator:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateAllowSigningID; + } + messageSetup:^(es_message_t *msg) { + msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID); + msg->event.exec.target->signing_id = MakeESStringToken(kAllowedSigningID); + }]; +} + - (void)testBinaryWithCDHashBlockRuleIsBlockedInLockdownMode { [self checkBinaryExecution:@"banned_cdhash" wantResult:ES_AUTH_RESULT_DENY @@ -416,6 +504,19 @@ - (void)testBinaryWithCDHashBlockRuleIsBlockedInLockdownMode { }]; } +- (void)testBinaryWithCDHashBlockRuleIsBlockedInStandaloneMode { + [self checkBinaryExecution:@"banned_cdhash" + wantResult:ES_AUTH_RESULT_DENY + clientMode:SNTClientModeStandalone + cdValidator:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateBlockCDHash; + } + messageSetup:^(es_message_t *msg) { + SetBinaryDataFromHexString(kBlockedCDHash, msg->event.exec.target->cdhash, + sizeof(msg->event.exec.target->cdhash)); + }]; +} + - (void)testBinaryWithCDHashBlockRuleIsBlockedInMonitorMode { [self checkBinaryExecution:@"banned_cdhash" wantResult:ES_AUTH_RESULT_DENY @@ -455,6 +556,19 @@ - (void)testBinaryWithCDHashAllowRuleIsAllowedInLockdownMode { }]; } +- (void)testBinaryWithCDHashAllowRuleIsAllowedInStandaloneMode { + [self checkBinaryExecution:@"allowed_cdhash" + wantResult:ES_AUTH_RESULT_ALLOW + clientMode:SNTClientModeMonitor + cdValidator:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateAllowCDHash; + } + messageSetup:^(es_message_t *msg) { + SetBinaryDataFromHexString(kAllowedCDHash, msg->event.exec.target->cdhash, + sizeof(msg->event.exec.target->cdhash)); + }]; +} + - (void)testBinaryWithSHA256AllowRuleAndBlockedTeamIDRuleIsAllowedInLockdownMode { [self checkBinaryExecution:@"banned_teamid_allowed_binary" wantResult:ES_AUTH_RESULT_ALLOW @@ -468,6 +582,19 @@ - (void)testBinaryWithSHA256AllowRuleAndBlockedTeamIDRuleIsAllowedInLockdownMode }]; } +- (void)testBinaryWithSHA256AllowRuleAndBlockedTeamIDRuleIsAllowedInStandaloneMode { + [self checkBinaryExecution:@"banned_teamid_allowed_binary" + wantResult:ES_AUTH_RESULT_ALLOW + clientMode:SNTClientModeStandalone + cdValidator:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateAllowBinary; + } + messageSetup:^(es_message_t *msg) { + msg->event.exec.target->team_id = MakeESStringToken(kBlockedTeamID); + msg->event.exec.target->signing_id = MakeESStringToken(kNoRuleMatchSigningID); + }]; +} + - (void)testBinaryWithSHA256AllowRuleAndBlockedTeamIDRuleIsAllowedInMonitorMode { [self checkBinaryExecution:@"banned_teamid_allowed_binary" wantResult:ES_AUTH_RESULT_ALLOW @@ -490,6 +617,15 @@ - (void)testBinaryWithoutBlockOrAllowRuleIsBlockedInLockdownMode { }]; } +- (void)testBinaryWithoutBlockOrAllowRuleIsBlockedInStandaloneMode { + [self checkBinaryExecution:@"noop" + wantResult:ES_AUTH_RESULT_DENY + clientMode:SNTClientModeStandalone + cdValidator:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateBlockUnknown; + }]; +} + - (void)testBinaryWithoutBlockOrAllowRuleIsAllowedInMonitorMode { [self checkBinaryExecution:@"noop" wantResult:ES_AUTH_RESULT_ALLOW diff --git a/Source/santasyncservice/SNTSyncEventUpload.mm b/Source/santasyncservice/SNTSyncEventUpload.mm index 95a615e6..de1fcc03 100644 --- a/Source/santasyncservice/SNTSyncEventUpload.mm +++ b/Source/santasyncservice/SNTSyncEventUpload.mm @@ -176,6 +176,9 @@ - (BOOL)uploadEvents:(NSArray *)events { c->set_valid_until([cert.validUntil timeIntervalSince1970]); } + // TODO: Add support the for Standalone Approval field so that a sync service + // can be notified that a user self approved a binary. + return *e; } diff --git a/Source/santasyncservice/SNTSyncPreflight.mm b/Source/santasyncservice/SNTSyncPreflight.mm index 3fff3cde..d02e87fc 100644 --- a/Source/santasyncservice/SNTSyncPreflight.mm +++ b/Source/santasyncservice/SNTSyncPreflight.mm @@ -110,6 +110,7 @@ - (BOOL)sync { switch (cm) { case SNTClientModeMonitor: req->set_client_mode(::pbv1::MONITOR); break; case SNTClientModeLockdown: req->set_client_mode(::pbv1::LOCKDOWN); break; + case SNTClientModeStandalone: req->set_client_mode(::pbv1::STANDALONE); break; default: break; } }]; @@ -181,6 +182,7 @@ - (BOOL)sync { switch (resp.client_mode()) { case ::pbv1::MONITOR: self.syncState.clientMode = SNTClientModeMonitor; break; case ::pbv1::LOCKDOWN: self.syncState.clientMode = SNTClientModeLockdown; break; + case ::pbv1::STANDALONE: self.syncState.clientMode = SNTClientModeStandalone; break; default: break; } diff --git a/Source/santasyncservice/SNTSyncTest.mm b/Source/santasyncservice/SNTSyncTest.mm index 95da7995..a5399a2c 100644 --- a/Source/santasyncservice/SNTSyncTest.mm +++ b/Source/santasyncservice/SNTSyncTest.mm @@ -632,6 +632,18 @@ - (void)testPreflightLockdown { XCTAssertEqual(self.syncState.clientMode, SNTClientModeLockdown); } +- (void)testPreflightStandalone { + [self setupDefaultDaemonConnResponses]; + SNTSyncPreflight *sut = [[SNTSyncPreflight alloc] initWithState:self.syncState]; + + NSData *respData = [self dataFromFixture:@"sync_preflight_standalone.json"]; + [self stubRequestBody:respData response:nil error:nil validateBlock:nil]; + + [sut sync]; + + XCTAssertEqual(self.syncState.clientMode, SNTClientModeStandalone); +} + #pragma mark - SNTSyncEventUpload Tests - (void)testEventUploadBasic { diff --git a/Source/santasyncservice/testdata/sync_preflight_standalone.json b/Source/santasyncservice/testdata/sync_preflight_standalone.json new file mode 100644 index 00000000..f8b9bb73 --- /dev/null +++ b/Source/santasyncservice/testdata/sync_preflight_standalone.json @@ -0,0 +1 @@ +{"whitelist_regex": null, "client_mode": "STANDALONE", "blacklist_regex": null, "batch_size": 100} diff --git a/docs/concepts/mode.md b/docs/concepts/mode.md index 59fab4cc..73b99dcf 100644 --- a/docs/concepts/mode.md +++ b/docs/concepts/mode.md @@ -27,6 +27,15 @@ Running Santa in Lockdown Mode will stop all blocked binaries and additionally will prevent all unknown binaries from running. This means that if the binary has no rules or scopes that apply, then it will be blocked. +##### Standalone mode + +When Santa is in Standalone Mode it will allow the user to approve their own binaries provided they authenticate biometrically with Touch ID. Upon a successful authentication Santa will then add a `SigningID` rule for the binary if it is validly signed and a `BINARY` if it is not signed at all. + +When paired with Lockdown, it allows a user to quickly self approve in lieu of using a sync service. If one is using a sync service Events will still be sent up to that sync service. + +{: .note} +Standalone mode will not override explicit block rules when Santa is configured to use a sync service nor will it override static rules. + ##### Changing Modes There are two ways to change the running mode: changing the configuration diff --git a/docs/deployment/configuration.md b/docs/deployment/configuration.md index ba73d85a..1d080e1b 100644 --- a/docs/deployment/configuration.md +++ b/docs/deployment/configuration.md @@ -22,7 +22,7 @@ also known as mobileconfig files, which are in an Apple-specific XML format. | Key | Value Type | Description | | ---------------------------------- | ---------- | ---------------------------------------- | -| ClientMode\* | Integer | 1 = MONITOR, 2 = LOCKDOWN, defaults to MONITOR | +| ClientMode\* | Integer | 1 = MONITOR, 2 = LOCKDOWN, 3 = STANDALONE defaults to MONITOR | | FailClosed | Bool | If true and the ClientMode is LOCKDOWN: execution will be denied when there is an error reading or processing an executable file and when Santa has to make a default response just prior to deadlines expiring. Defaults to false. | | FileChangesRegex\* | String | The regex of paths to log file changes. Regexes are specified in ICU format. | | AllowedPathRegex\* | String | A regex to allow if the binary, certificate, or Team ID scopes did not allow/block execution. Regexes are specified in ICU format. |