Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Phishing Detection Feature Flag + Preferences #3156

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
public enum FeatureFlag: String {
case debugMenu
case sslCertificatesBypass
case phishingDetection
case phishingDetectionPreferences

/// Add experimental atb parameter to SERP queries for internal users to display Privacy Reminder
/// https://app.asana.com/0/1199230911884351/1205979030848528/f
Expand All @@ -43,6 +45,10 @@
return .internalOnly
case .sslCertificatesBypass:
return .remoteReleasable(.subfeature(sslCertificatesSubfeature.allowBypass))
case .phishingDetection:
return .remoteReleasable(.subfeature(phishingDetectionSubfeature.allowErrorPage))

Check failure on line 49 in DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift

View workflow job for this annotation

GitHub Actions / Make Release Build

cannot find 'phishingDetectionSubfeature' in scope

Check failure on line 49 in DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift

View workflow job for this annotation

GitHub Actions / Make Release Build

cannot find 'phishingDetectionSubfeature' in scope
case .phishingDetectionPreferences:
return .remoteReleasable(.subfeature(phishingDetectionSubfeature.allowPreferencesToggle))

Check failure on line 51 in DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift

View workflow job for this annotation

GitHub Actions / Make Release Build

cannot find 'phishingDetectionSubfeature' in scope

Check failure on line 51 in DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift

View workflow job for this annotation

GitHub Actions / Make Release Build

cannot find 'phishingDetectionSubfeature' in scope
case .deduplicateLoginsOnImport:
return .remoteReleasable(.subfeature(AutofillSubfeature.deduplicateLoginsOnImport))
case .freemiumPIR:
Expand Down
150 changes: 150 additions & 0 deletions DuckDuckGo/PhishingDetection/PhishingDetection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//
// PhishingDetection.swift
//
// 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 PhishingDetection
import Combine
import BrowserServicesKit
import PixelKit
import Common

/// PhishingDetection is implemented using two datasets that are embedded into the client as a Bundle in `DataProvider`,
/// and kept up to date by `DataActivities` and `UpdateManager`. If the feature is disabled in `Preferences`,
/// we stop the background tasks and don't check `isMalicious` on any URLs.
protocol PhishingSiteDetecting {
func checkIsMaliciousIfEnabled(url: URL) async -> Bool
}

public class PhishingDetection: PhishingSiteDetecting {
static let shared: PhishingDetection = PhishingDetection()
private var detector: PhishingDetecting
private var updateManager: PhishingDetectionUpdateManaging
private var dataActivities: PhishingDetectionDataActivityHandling
private var detectionPreferences: PhishingDetectionPreferences
private var dataStore: PhishingDetectionDataSaving
private var featureFlagger: FeatureFlagger
private var config: PrivacyConfiguration
private var cancellable: AnyCancellable?
private let revision: Int
private let filterSetURL: URL
private let filterSetDataSHA: String
private let hashPrefixURL: URL
private let hashPrefixDataSHA: String

private init(
revision: Int = 1653367,
filterSetURL: URL = Bundle.main.url(forResource: "filterSet", withExtension: "json")!,
filterSetDataSHA: String = "edd913cb0a579c2b163a01347531ed78976bfaf1d14b96a658c4a39d34a70ffc",
hashPrefixURL: URL = Bundle.main.url(forResource: "hashPrefixes", withExtension: "json")!,
hashPrefixDataSHA: String = "c61349d196c46db9155ca654a0d33368ee0f33766fcd63e5a20f1d5c92026dc5",
detectionClient: PhishingDetectionAPIClient = PhishingDetectionAPIClient(),
dataProvider: PhishingDetectionDataProvider? = nil,
dataStore: PhishingDetectionDataSaving? = nil,
detector: PhishingDetecting? = nil,
updateManager: PhishingDetectionUpdateManaging? = nil,
dataActivities: PhishingDetectionDataActivityHandling? = nil,
detectionPreferences: PhishingDetectionPreferences = PhishingDetectionPreferences.shared,
featureFlagger: FeatureFlagger? = nil
) {
self.revision = revision
self.filterSetURL = filterSetURL
self.filterSetDataSHA = filterSetDataSHA
self.hashPrefixURL = hashPrefixURL
self.hashPrefixDataSHA = hashPrefixDataSHA

let resolvedDataProvider = dataProvider ?? PhishingDetectionDataProvider(
revision: revision,
filterSetURL: filterSetURL,
filterSetDataSHA: filterSetDataSHA,
hashPrefixURL: hashPrefixURL,
hashPrefixDataSHA: hashPrefixDataSHA
)
self.dataStore = dataStore ?? PhishingDetectionDataStore(dataProvider: resolvedDataProvider)
self.detector = detector ?? PhishingDetector(apiClient: detectionClient, dataStore: self.dataStore, eventMapping:
EventMapping<PhishingDetectionEvents> {event, _, params, _ in

Check failure on line 79 in DuckDuckGo/PhishingDetection/PhishingDetection.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Unused parameter in a closure should be replaced with _ (unused_closure_parameter)
switch event {
case .errorPageShown(clientSideHit: let clientSideHit):
PixelKit.fire(PhishingDetectionEvents.errorPageShown(clientSideHit: clientSideHit))
case .iframeLoaded:
PixelKit.fire(PhishingDetectionEvents.iframeLoaded)
case .visitSite:
PixelKit.fire(PhishingDetectionEvents.visitSite)
case .updateTaskFailed48h(error: let error):
PixelKit.fire(PhishingDetectionEvents.updateTaskFailed48h(error: error))
}
})
self.updateManager = updateManager ?? PhishingDetectionUpdateManager(client: detectionClient, dataStore: self.dataStore)
self.dataActivities = dataActivities ?? PhishingDetectionDataActivities(phishingDetectionDataProvider: resolvedDataProvider, updateManager: self.updateManager)
self.detectionPreferences = detectionPreferences
self.featureFlagger = NSApp.delegateTyped.featureFlagger
self.config = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager.privacyConfig
if let featureFlagger = featureFlagger,
featureFlagger.isFeatureOn(.phishingDetection),
self.detectionPreferences.isEnabled {
startUpdateTasks()
}
self.setupBindings()
}

convenience init(
dataActivities: PhishingDetectionDataActivityHandling,
dataStore: PhishingDetectionDataSaving,
detector: PhishingDetecting
) {
self.init(
dataStore: dataStore,
detector: detector,
dataActivities: dataActivities
)
}

private func setupBindings() {
cancellable = detectionPreferences.$isEnabled.sink { [weak self] isEnabled in
self?.handleIsEnabledChange(enabled: isEnabled)
}
}

public func checkIsMaliciousIfEnabled(url: URL) async -> Bool {
if config.isFeature(.phishingDetection, enabledForDomain: url.host),
detectionPreferences.isEnabled {
return await detector.isMalicious(url: url)
} else {
return false
}
}

private func handleIsEnabledChange(enabled: Bool) {
if enabled {
startUpdateTasks()
} else {
stopUpdateTasks()
}
}

private func startUpdateTasks() {
dataActivities.start()
}

private func stopUpdateTasks() {
dataActivities.stop()
}

deinit {
cancellable?.cancel()
}
}
50 changes: 50 additions & 0 deletions DuckDuckGo/PhishingDetection/PhishingDetectionPreferences.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// PhishingDetectionPreferences.swift
//
// 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 Combine

protocol PhishingDetectionPreferencesPersistor {
var isEnabled: Bool { get set }
}

struct PhishingDetectionPreferencesUserDefaultsPersistor: PhishingDetectionPreferencesPersistor {

@UserDefaultsWrapper(key: .phishingDetectionEnabled, defaultValue: true)
var isEnabled: Bool
}

final class PhishingDetectionPreferences: ObservableObject {

static let shared = PhishingDetectionPreferences()

@Published
var isEnabled: Bool {
didSet {
persistor.isEnabled = isEnabled
}
}

init(persistor: PhishingDetectionPreferencesPersistor = PhishingDetectionPreferencesUserDefaultsPersistor()) {
self.persistor = persistor
self.isEnabled = persistor.isEnabled
}

private var persistor: PhishingDetectionPreferencesPersistor
private var cancellables: Set<AnyCancellable> = []
}
31 changes: 31 additions & 0 deletions DuckDuckGo/PhishingDetection/PhishingDetectionStateManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// PhishingDetectionStateManager.swift
//
// 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

public protocol PhishingTabStateManaging {
var didBypassError: Bool { get set }
var isShowingPhishingError: Bool { get set }
}

public class PhishingTabStateManager: PhishingTabStateManaging {
public var didBypassError: Bool = false
public var isShowingPhishingError: Bool = false

public init(){}
}
16 changes: 16 additions & 0 deletions DuckDuckGo/Preferences/View/PreferencesGeneralView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@
@ObservedObject var searchModel: SearchPreferences
@ObservedObject var tabsModel: TabsPreferences
@ObservedObject var dataClearingModel: DataClearingPreferences
@ObservedObject var phishingDetectionModel: PhishingDetectionPreferences
@State private var showingCustomHomePageSheet = false
@State private var isAddedToDock = false
var dockCustomizer: DockCustomizer
let featureFlagger = NSApp.delegateTyped.featureFlagger

var body: some View {
PreferencePane(UserText.general) {
Expand Down Expand Up @@ -190,6 +192,20 @@
isOn: $downloadsModel.alwaysRequestDownloadLocation)
}
}

// SECTION 7: Phishing Detection
if featureFlagger.isFeatureOn(.phishingDetectionPreferences) {
PreferencePaneSection(UserText.phishingDetectionHeader) {

Check failure on line 198 in DuckDuckGo/Preferences/View/PreferencesGeneralView.swift

View workflow job for this annotation

GitHub Actions / Make Release Build

type 'UserText' has no member 'phishingDetectionHeader'

Check failure on line 198 in DuckDuckGo/Preferences/View/PreferencesGeneralView.swift

View workflow job for this annotation

GitHub Actions / Make Release Build

type 'UserText' has no member 'phishingDetectionHeader'
PreferencePaneSubSection {
ToggleMenuItem(UserText.phishingDetectionIsEnabled,

Check failure on line 200 in DuckDuckGo/Preferences/View/PreferencesGeneralView.swift

View workflow job for this annotation

GitHub Actions / Make Release Build

type 'UserText' has no member 'phishingDetectionIsEnabled'

Check failure on line 200 in DuckDuckGo/Preferences/View/PreferencesGeneralView.swift

View workflow job for this annotation

GitHub Actions / Make Release Build

type 'UserText' has no member 'phishingDetectionIsEnabled'
isOn: $phishingDetectionModel.isEnabled)
}.padding(.bottom, 5)
Text(UserText.phishingDetectionEnabledWarning)
.font(.footnote)
.foregroundColor(.red)
.padding(.top, 5)
}
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions DuckDuckGo/Preferences/View/PreferencesRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
searchModel: SearchPreferences.shared,
tabsModel: TabsPreferences.shared,
dataClearingModel: DataClearingPreferences.shared,
phishingDetectionModel: PhishingDetectionPreferences.shared,

Check failure on line 97 in DuckDuckGo/Preferences/View/PreferencesRootView.swift

View workflow job for this annotation

GitHub Actions / Test (Sandbox)

cannot find 'PhishingDetectionPreferences' in scope

Check failure on line 97 in DuckDuckGo/Preferences/View/PreferencesRootView.swift

View workflow job for this annotation

GitHub Actions / Make Release Build

cannot find 'PhishingDetectionPreferences' in scope

Check failure on line 97 in DuckDuckGo/Preferences/View/PreferencesRootView.swift

View workflow job for this annotation

GitHub Actions / Make Release Build

cannot find 'PhishingDetectionPreferences' in scope
dockCustomizer: DockCustomizer())
case .sync:
SyncView()
Expand Down
Loading