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

Malicious site protection navigation detection integration #3730

Open
wants to merge 12 commits into
base: alessandro/malicious-site-protection-feature-flags
Choose a base branch
from
Open
68 changes: 56 additions & 12 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

18 changes: 13 additions & 5 deletions DuckDuckGo/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2320,8 +2320,8 @@ extension MainViewController: TabDelegate {
return newTab.webView
}

func tabDidRequestClose(_ tab: TabViewController) {
closeTab(tab.tabModel)
func tabDidRequestClose(_ tab: TabViewController, shouldCreateEmptyTabAtSamePosition: Bool) {
closeTab(tab.tabModel, andOpenEmptyOneAtSamePosition: shouldCreateEmptyTabAtSamePosition)
}

func tabLoadingStateDidChange(tab: TabViewController) {
Expand Down Expand Up @@ -2571,12 +2571,20 @@ extension MainViewController: TabSwitcherDelegate {
showFireButtonPulse()
}
}
func closeTab(_ tab: Tab) {

func closeTab(_ tab: Tab, andOpenEmptyOneAtSamePosition shouldOpen: Bool = false) {
guard let index = tabManager.model.indexOf(tab: tab) else { return }
hideSuggestionTray()
hideNotificationBarIfBrokenSitePromptShown()
tabManager.remove(at: index)

if shouldOpen {
let newTab = Tab()
tabManager.replaceTab(at: index, withNewTab: newTab)
tabManager.selectTab(newTab)
} else {
tabManager.remove(at: index)
}

updateCurrentTab()
tabsBarController?.refresh(tabsModel: tabManager.model)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// MaliciousSiteProtectionManager+Config.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 MaliciousSiteProtection

extension MaliciousSiteProtectionManager {

static func fileName(for dataType: MaliciousSiteProtection.DataManager.StoredDataType) -> String {
switch (dataType, dataType.threatKind) {
case (.hashPrefixSet, .phishing): "phishingHashPrefixes.json"
case (.filterSet, .phishing): "phishingFilterSet.json"
case (.hashPrefixSet, .malware): "malwareHashPrefixes.json"
case (.filterSet, .malware): "malwareFilterSet.json"
}
}

static func updateInterval(for dataKind: MaliciousSiteProtection.DataManager.StoredDataType) -> TimeInterval {
switch dataKind {
case .hashPrefixSet: .minutes(20)
case .filterSet: .hours(12)
}
}

struct EmbeddedDataProvider: MaliciousSiteProtection.EmbeddedDataProviding {

// swiftlint:disable:next nesting
private enum Constants {
static let embeddedDataRevision = 1696473
static let phishingEmbeddedHashPrefixDataSHA = "cdb609c37e950b7d0dcdaa80ae4071cf2c87223cfdd189caafae723722bd3158"
static let phishingEmbeddedFilterSetDataSHA = "4e52518aba04b0fd360fada76c9899001d3137d4a745cc13c484a54115a0fcd8"
static let malwareEmbeddedHashPrefixDataSHA = "6b5eb296e9e10ae9ea41c5b5356f532226d647e4f3b832c30ac670102446ea7a"
static let malwareEmbeddedFilterSetDataSHA = "4dc971fffaf244ee99267f28222a2c116743e35ef837dcbc0199693ed6a691cd"
}

func revision(for dataType: MaliciousSiteProtection.DataManager.StoredDataType) -> Int {
Constants.embeddedDataRevision
}

func url(for dataType: MaliciousSiteProtection.DataManager.StoredDataType) -> URL {
let fileName = fileName(for: dataType)
guard let url = Bundle.main.url(forResource: fileName, withExtension: nil) else {
fatalError("Could not find embedded data file \"\(fileName)\"")
}
return url
}

func hash(for dataType: MaliciousSiteProtection.DataManager.StoredDataType) -> String {
switch (dataType, dataType.threatKind) {
case (.hashPrefixSet, .phishing): Constants.phishingEmbeddedHashPrefixDataSHA
case (.filterSet, .phishing): Constants.phishingEmbeddedFilterSetDataSHA
case (.hashPrefixSet, .malware): Constants.malwareEmbeddedHashPrefixDataSHA
case (.filterSet, .malware): Constants.malwareEmbeddedFilterSetDataSHA
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,124 @@
// limitations under the License.
//

import BrowserServicesKit
import Combine
import Common
import Foundation
import MaliciousSiteProtection
import Networking
import PixelKit

final class MaliciousSiteProtectionManager: MaliciousSiteDetecting {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private let detector: MaliciousSiteDetecting
private let updateManager: MaliciousSiteProtection.UpdateManager
private let maliciousSiteProtectionFeatureFlagger: MaliciousSiteProtectionFeatureFlagger
private let preferencesManager: MaliciousSiteProtectionPreferencesPublishing

private var preferencesManagerCancellable: AnyCancellable?
private var updateTask: Task<Void, Error>?

var isBackgroundUpdatesEnabled: Bool { updateTask != nil }

private static let debugEvents = EventMapping<MaliciousSiteProtection.Event> { event, _, _, _ in
PixelKit.fire(event)
}

init(
apiEnvironment: MaliciousSiteDetector.APIEnvironment = .production,
apiService: APIService = DefaultAPIService(urlSession: .shared),
embeddedDataProvider: MaliciousSiteProtection.EmbeddedDataProviding = EmbeddedDataProvider(),
dataManager: MaliciousSiteProtection.DataManager? = nil,
detector: MaliciousSiteProtection.MaliciousSiteDetecting? = nil,
preferencesManager: MaliciousSiteProtectionPreferencesPublishing = MaliciousSiteProtectionPreferencesManager(),
maliciousSiteProtectionFeatureFlagger: MaliciousSiteProtectionFeatureFlagger = MaliciousSiteProtectionFeatureFlags(),
updateIntervalProvider: UpdateManager.UpdateIntervalProvider? = nil
) {
let embeddedDataProvider = EmbeddedDataProvider()

let dataManager = dataManager ?? MaliciousSiteProtection.DataManager(
fileStore: MaliciousSiteProtection.FileStore(
dataStoreURL: FileManager.default.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
)
.first!
),
embeddedDataProvider: embeddedDataProvider,
fileNameProvider: Self.fileName(for:)
)

self.detector = detector ?? MaliciousSiteDetector(
apiEnvironment: apiEnvironment,
service: apiService,
dataManager: dataManager,
eventMapping: Self.debugEvents
)

self.updateManager = MaliciousSiteProtection.UpdateManager(
apiEnvironment: apiEnvironment,
service: apiService,
dataManager: dataManager,
updateIntervalProvider: updateIntervalProvider ?? Self.updateInterval
)

self.preferencesManager = preferencesManager
self.maliciousSiteProtectionFeatureFlagger = maliciousSiteProtectionFeatureFlagger

self.setupBindings()
}

}

// MARK: - Public

extension MaliciousSiteProtectionManager {

func evaluate(_ url: URL) async -> ThreatKind? {
try? await Task.sleep(interval: 0.3)

switch url.absoluteString {
case "http://privacy-test-pages.site/security/badware/phishing.html":
return .phishing
case "http://privacy-test-pages.site/security/badware/malware.html":
return .malware
default:
guard
maliciousSiteProtectionFeatureFlagger.shouldDetectMaliciousThreat(forDomain: url.host),
preferencesManager.isEnabled
else {
return .none
}

return await detector.evaluate(url)
}

}

// MARK: - Private

private extension MaliciousSiteProtectionManager {

func setupBindings() {
guard maliciousSiteProtectionFeatureFlagger.isMaliciousSiteProtectionEnabled else { return }
subscribeToDetectionPreferences()
}

func subscribeToDetectionPreferences() {
preferencesManagerCancellable = preferencesManager
.isEnabledPublisher
.sink { [weak self] isEnabled in
self?.handleIsEnabledChange(enabled: isEnabled)
}
}

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

func startUpdateTasks() {
updateTask = updateManager.startPeriodicUpdates()
}

func stopUpdateTasks() {
updateTask?.cancel()
updateTask = nil
}

}
Loading
Loading