diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 7ec10ba680..21c81ae32d 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -63,6 +63,10 @@ public enum FeatureFlag: String { /// https://app.asana.com/0/0/1208767141940869/f case freeTrials + + /// Feature flag to enable / disable phishing and malware protection + /// https://app.asana.com/0/1206329551987282/1207149365636877/f + case maliciousSiteProtection } extension FeatureFlag: FeatureFlagDescribing { @@ -146,6 +150,8 @@ extension FeatureFlag: FeatureFlagDescribing { return .remoteReleasable(.subfeature(PrivacyProSubfeature.isLaunchedROWOverride)) case .freeTrials: return .remoteDevelopment(.subfeature(PrivacyProSubfeature.freeTrials)) + case .maliciousSiteProtection: + return .internalOnly() } } } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a4ca078050..b8f785c575 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -749,6 +749,8 @@ 98F3A1D8217B37010011A0D4 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F3A1D7217B37010011A0D4 /* Theme.swift */; }; 98F6EA472863124100720957 /* ContentBlockerRulesLists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */; }; 98F78B8E22419093007CACF4 /* ThemableNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */; }; + 9F06EB752D09E8D200905426 /* MaliciousSiteProtectionFeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB732D09E8D200905426 /* MaliciousSiteProtectionFeatureFlags.swift */; }; + 9F06EB7B2D09EC2000905426 /* MaliciousSiteProtectionFeatureFlagsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06EB792D09EC2000905426 /* MaliciousSiteProtectionFeatureFlagsTests.swift */; }; 9F16230B2CA0F0190093C4FC /* DebouncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */; }; 9F1798572CD2443F0073018B /* AddToDockPromoViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1798562CD2443F0073018B /* AddToDockPromoViewModelTests.swift */; }; 9F23B8012C2BC94400950875 /* OnboardingBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */; }; @@ -2618,6 +2620,8 @@ 98F3A1D7217B37010011A0D4 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; 98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentBlockerRulesLists.swift; sourceTree = ""; }; 98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemableNavigationController.swift; sourceTree = ""; }; + 9F06EB732D09E8D200905426 /* MaliciousSiteProtectionFeatureFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionFeatureFlags.swift; sourceTree = ""; }; + 9F06EB792D09EC2000905426 /* MaliciousSiteProtectionFeatureFlagsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaliciousSiteProtectionFeatureFlagsTests.swift; sourceTree = ""; }; 9F16230A2CA0F0190093C4FC /* DebouncerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncerTests.swift; sourceTree = ""; }; 9F1798562CD2443F0073018B /* AddToDockPromoViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToDockPromoViewModelTests.swift; sourceTree = ""; }; 9F23B8002C2BC94400950875 /* OnboardingBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackground.swift; sourceTree = ""; }; @@ -4979,6 +4983,22 @@ name = Themes; sourceTree = ""; }; + 9F06EB742D09E8D200905426 /* FeatureFlags */ = { + isa = PBXGroup; + children = ( + 9F06EB732D09E8D200905426 /* MaliciousSiteProtectionFeatureFlags.swift */, + ); + path = FeatureFlags; + sourceTree = ""; + }; + 9F06EB7A2D09EC2000905426 /* MaliciousSiteProtection */ = { + isa = PBXGroup; + children = ( + 9F06EB792D09EC2000905426 /* MaliciousSiteProtectionFeatureFlagsTests.swift */, + ); + path = MaliciousSiteProtection; + sourceTree = ""; + }; 9F23B7FF2C2BABE000950875 /* OnboardingIntro */ = { isa = PBXGroup; children = ( @@ -5033,6 +5053,7 @@ 9F254AA92CF47CD30063B308 /* MaliciousSiteProtection */ = { isa = PBXGroup; children = ( + 9F06EB742D09E8D200905426 /* FeatureFlags */, 9F254AAA2CF47DD50063B308 /* MaliciousSiteProtectionManager.swift */, ); path = MaliciousSiteProtection; @@ -6090,6 +6111,7 @@ 83134D7F20E2E013006CE65D /* Feedback */, 8588026724E4249800C24AB6 /* iPad */, 851DFD88212C5ED600D95F20 /* Main */, + 9F06EB7A2D09EC2000905426 /* MaliciousSiteProtection */, EE56DE3A2A6038F500375C41 /* NetworkProtection */, 6F03CAFF2C32ED22004179A8 /* NewTabPage */, F1D477C71F2139210031ED49 /* OmniBar */, @@ -8078,6 +8100,7 @@ F13B4BC01F180D8A00814661 /* TabsModel.swift in Sources */, 8598D2E02CEB98B500C45685 /* Favicons.swift in Sources */, 8598D2E12CEB98B500C45685 /* NotFoundCachingDownloader.swift in Sources */, + 9F06EB752D09E8D200905426 /* MaliciousSiteProtectionFeatureFlags.swift in Sources */, 8598D2E22CEB98B500C45685 /* FaviconRequestModifier.swift in Sources */, 8598D2E32CEB98B500C45685 /* FaviconUserScript.swift in Sources */, 8598D2E42CEB98B500C45685 /* FaviconSourcesProvider.swift in Sources */, @@ -8360,6 +8383,7 @@ 83EDCC411F86B89C005CDFCD /* StatisticsLoaderTests.swift in Sources */, 564DE4572C4150E600D23241 /* NewTabPageControllerDaxDialogTests.swift in Sources */, C14882E327F20D9A00D59F0C /* BookmarksExporterTests.swift in Sources */, + 9F06EB7B2D09EC2000905426 /* MaliciousSiteProtectionFeatureFlagsTests.swift in Sources */, 85C29708247BDD060063A335 /* DaxDialogsBrowsingSpecTests.swift in Sources */, 9FE05CF12C36468A00D9046B /* OnboardingPixelReporterTests.swift in Sources */, 9FDEC7B42C8FD62F00C7A692 /* OnboardingAddressBarPositionPickerViewModelTests.swift in Sources */, diff --git a/DuckDuckGo/MaliciousSiteProtection/FeatureFlags/MaliciousSiteProtectionFeatureFlags.swift b/DuckDuckGo/MaliciousSiteProtection/FeatureFlags/MaliciousSiteProtectionFeatureFlags.swift new file mode 100644 index 0000000000..748946a597 --- /dev/null +++ b/DuckDuckGo/MaliciousSiteProtection/FeatureFlags/MaliciousSiteProtectionFeatureFlags.swift @@ -0,0 +1,60 @@ +// +// MaliciousSiteProtectionFeatureFlags.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. 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. +// + +import Foundation +import BrowserServicesKit +import Core + +protocol MaliciousSiteProtectionFeatureFlagger { + /// A Boolean value indicating whether malicious site protection is enabled. + /// - Returns: `true` if malicious site protection is enabled; otherwise, `false`. + var isMaliciousSiteProtectionEnabled: Bool { get } + + /// Checks if should detect malicious threats for a specific domain. + /// - Parameter domain: The domain to check for malicious threat. + /// - Returns: `true` if should check for malicious threats for the specified domain; otherwise, `false`. + func shouldDetectMaliciousThreat(forDomain domain: String?) -> Bool +} + +final class MaliciousSiteProtectionFeatureFlags { + private let featureFlagger: FeatureFlagger + private let privacyConfigManager: PrivacyConfigurationManaging + + init( + featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger, + privacyConfigManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager + ) { + self.featureFlagger = featureFlagger + self.privacyConfigManager = privacyConfigManager + } +} + +// MARK: - MaliciousSiteProtectionFeatureFlagger + +extension MaliciousSiteProtectionFeatureFlags: MaliciousSiteProtectionFeatureFlagger { + + var isMaliciousSiteProtectionEnabled: Bool { + featureFlagger.isFeatureOn(.maliciousSiteProtection) + } + + func shouldDetectMaliciousThreat(forDomain domain: String?) -> Bool { + privacyConfigManager.privacyConfig.isFeature(.maliciousSiteProtection, enabledForDomain: domain) + } + +} diff --git a/DuckDuckGoTests/MaliciousSiteProtection/MaliciousSiteProtectionFeatureFlagsTests.swift b/DuckDuckGoTests/MaliciousSiteProtection/MaliciousSiteProtectionFeatureFlagsTests.swift new file mode 100644 index 0000000000..eb17260de5 --- /dev/null +++ b/DuckDuckGoTests/MaliciousSiteProtection/MaliciousSiteProtectionFeatureFlagsTests.swift @@ -0,0 +1,112 @@ +// +// MaliciousSiteProtectionFeatureFlagsTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. 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. +// + +import Testing +import BrowserServicesKit +@testable import DuckDuckGo + +@Suite("Malicious Site Protection - Feature Flags", .serialized) +final class MaliciousSiteProtectionFeatureFlagsTests { + private var sut: MaliciousSiteProtectionFeatureFlags! + private var featureFlaggerMock: MockFeatureFlagger! + private var configurationManagerMock: PrivacyConfigurationManagerMock! + + init() async throws { + featureFlaggerMock = MockFeatureFlagger() + configurationManagerMock = PrivacyConfigurationManagerMock() + sut = MaliciousSiteProtectionFeatureFlags(featureFlagger: featureFlaggerMock, privacyConfigManager: configurationManagerMock) + } + + deinit { + featureFlaggerMock = nil + configurationManagerMock = nil + sut = nil + } + + // MARK: - Web Error Page + + @Test("Check Threat Detection Enabled") + func whenThreatDetectionEnabled_AndFeatureFlagIsOn_ThenReturnTrue() throws { + // GIVEN + featureFlaggerMock.enabledFeatureFlags = [.maliciousSiteProtection] + + // WHEN + let result = sut.isMaliciousSiteProtectionEnabled + + // THEN + #expect(result) + } + + @Test("Check Threat Detection Disabled") + func whenThreatDetectionEnabled_AndFeatureFlagIsOff_ThenReturnFalse() throws { + // GIVEN + featureFlaggerMock.enabledFeatureFlags = [] + + // WHEN + let result = sut.isMaliciousSiteProtectionEnabled + + // THEN + #expect(!result) + } + + @Test("Check Threat Detection Enabled For Domain") + func whenThreatDetectionEnabledForDomain_AndFeatureIsAvailableForDomain_ThenReturnTrue() throws { + // GIVEN + featureFlaggerMock.enabledFeatureFlags = [.maliciousSiteProtection] + let privacyConfigMock = try #require(configurationManagerMock.privacyConfig as? PrivacyConfigurationMock) + privacyConfigMock.enabledFeatures = [.maliciousSiteProtection: ["example.com"]] + let domain = "example.com" + + // WHEN + let result = sut.shouldDetectMaliciousThreat(forDomain: domain) + + // THEN + #expect(result) + } + + @Test("Check Threat Detection Disabled For Domain When Protection For Domain Is Not Enabled") + func whenThreatDetectionCalledEnabledForDomain_AndFeatureIsNotAvailableForDomain_ThenReturnFalse() throws { + // GIVEN + featureFlaggerMock.enabledFeatureFlags = [.maliciousSiteProtection] + let privacyConfigMock = try #require(configurationManagerMock.privacyConfig as? PrivacyConfigurationMock) + privacyConfigMock.enabledFeatures = [.maliciousSiteProtection: []] + let domain = "example.com" + + // WHEN + let result = sut.shouldDetectMaliciousThreat(forDomain: domain) + + // THEN + #expect(!result) + } + + @Test("Check Threat Detection Disabled For Domain When Feature Flag Is Off") + func whenThreatDetectionEnabledForDomain_AndPrivacyConfigFeatureFlagIsOn_AndThreatDetectionSubFeatureIsOff_ThenReturnTrue() throws { + // GIVEN + let privacyConfigMock = try #require(configurationManagerMock.privacyConfig as? PrivacyConfigurationMock) + privacyConfigMock.enabledFeatures = [.adClickAttribution: ["example.com"]] + let domain = "example.com" + + // WHEN + let result = sut.shouldDetectMaliciousThreat(forDomain: domain) + + // THEN + #expect(!result) + } + +}