diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/BrowserServicesKit-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/BrowserServicesKit-Package.xcscheme index 56a2ef845..44ea66164 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/BrowserServicesKit-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/BrowserServicesKit-Package.xcscheme @@ -491,9 +491,9 @@ buildForAnalyzing = "YES"> @@ -782,9 +782,9 @@ skipped = "NO"> diff --git a/Package.swift b/Package.swift index 46c0fd8fc..eadd9b6d1 100644 --- a/Package.swift +++ b/Package.swift @@ -42,7 +42,7 @@ let package = Package( .library(name: "PixelKitTestingUtilities", targets: ["PixelKitTestingUtilities"]), .library(name: "SpecialErrorPages", targets: ["SpecialErrorPages"]), .library(name: "DuckPlayer", targets: ["DuckPlayer"]), - .library(name: "PhishingDetection", targets: ["PhishingDetection"]), + .library(name: "MaliciousSiteProtection", targets: ["MaliciousSiteProtection"]), .library(name: "Onboarding", targets: ["Onboarding"]), .library(name: "BrokenSitePrompt", targets: ["BrokenSitePrompt"]), .library(name: "PageRefreshMonitor", targets: ["PageRefreshMonitor"]), @@ -249,6 +249,7 @@ let package = Package( "ContentBlocking", "Persistence", "BrowserServicesKit", + "MaliciousSiteProtection", .product(name: "PrivacyDashboardResources", package: "privacy-dashboard") ], path: "Sources/PrivacyDashboard", @@ -407,9 +408,12 @@ let package = Package( ] ), .target( - name: "PhishingDetection", + name: "MaliciousSiteProtection", dependencies: [ - "Common" + "Common", + "Networking", + "SpecialErrorPages", + "PixelKit", ], swiftSettings: [ .define("DEBUG", .when(configuration: .debug)) @@ -645,14 +649,13 @@ let package = Package( ), .testTarget( - name: "PhishingDetectionTests", + name: "MaliciousSiteProtectionTests", dependencies: [ - "PhishingDetection", - "PixelKit" + "MaliciousSiteProtection", ], resources: [ - .copy("Resources/hashPrefixes.json"), - .copy("Resources/filterSet.json") + .copy("Resources/phishingHashPrefixes.json"), + .copy("Resources/phishingFilterSet.json"), ] ), .testTarget( diff --git a/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift b/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift index fc79ba107..a29614bdb 100644 --- a/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift +++ b/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift @@ -50,7 +50,7 @@ public enum PrivacyFeature: String { case sslCertificates case brokenSiteReportExperiment case toggleReports - case phishingDetection + case maliciousSiteProtection case brokenSitePrompt case remoteMessaging case additionalCampaignPixelParams @@ -173,8 +173,8 @@ public enum DuckPlayerSubfeature: String, PrivacySubfeature { case enableDuckPlayer // iOS DuckPlayer rollout feature } -public enum PhishingDetectionSubfeature: String, PrivacySubfeature { - public var parent: PrivacyFeature { .phishingDetection } +public enum MaliciousSiteProtectionSubfeature: String, PrivacySubfeature { + public var parent: PrivacyFeature { .maliciousSiteProtection } case allowErrorPage case allowPreferencesToggle } diff --git a/Sources/PhishingDetection/PhishingDetectionClient.swift b/Sources/MaliciousSiteProtection/API/APIClient.swift similarity index 69% rename from Sources/PhishingDetection/PhishingDetectionClient.swift rename to Sources/MaliciousSiteProtection/API/APIClient.swift index 942075b71..50ddfbd6a 100644 --- a/Sources/PhishingDetection/PhishingDetectionClient.swift +++ b/Sources/MaliciousSiteProtection/API/APIClient.swift @@ -1,7 +1,7 @@ // -// PhishingDetectionClient.swift +// APIClient.swift // -// Copyright © 2023 DuckDuckGo. All rights reserved. +// 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. @@ -16,45 +16,14 @@ // limitations under the License. // -import Foundation import Common +import Foundation import os +import Networking -public struct HashPrefixResponse: Codable, Equatable { - public var insert: [String] - public var delete: [String] - public var revision: Int - public var replace: Bool - - public init(insert: [String], delete: [String], revision: Int, replace: Bool) { - self.insert = insert - self.delete = delete - self.revision = revision - self.replace = replace - } -} - -public struct FilterSetResponse: Codable, Equatable { - public var insert: [Filter] - public var delete: [Filter] - public var revision: Int - public var replace: Bool - - public init(insert: [Filter], delete: [Filter], revision: Int, replace: Bool) { - self.insert = insert - self.delete = delete - self.revision = revision - self.replace = replace - } -} - -public struct MatchResponse: Codable, Equatable { - public var matches: [Match] -} - -public protocol PhishingDetectionClientProtocol { - func getFilterSet(revision: Int) async -> FilterSetResponse - func getHashPrefixes(revision: Int) async -> HashPrefixResponse +public protocol APIClientProtocol { + func getFilterSet(revision: Int) async -> APIClient.FiltersChangeSetResponse + func getHashPrefixes(revision: Int) async -> APIClient.HashPrefixesChangeSetResponse func getMatches(hashPrefix: String) async -> [Match] } @@ -70,7 +39,7 @@ extension URLSessionProtocol { } } -public class PhishingDetectionAPIClient: PhishingDetectionClientProtocol { +public struct APIClient: APIClientProtocol { public enum Environment { case production @@ -113,20 +82,20 @@ public class PhishingDetectionAPIClient: PhishingDetectionClientProtocol { self.session = session } - public func getFilterSet(revision: Int) async -> FilterSetResponse { + public func getFilterSet(revision: Int) async -> FiltersChangeSetResponse { guard let url = createURL(for: .filterSet, revision: revision) else { logDebug("🔸 Invalid filterSet revision URL: \(revision)") - return FilterSetResponse(insert: [], delete: [], revision: revision, replace: false) + return FiltersChangeSetResponse(insert: [], delete: [], revision: revision, replace: false) } - return await fetch(url: url, responseType: FilterSetResponse.self) ?? FilterSetResponse(insert: [], delete: [], revision: revision, replace: false) + return await fetch(url: url, responseType: FiltersChangeSetResponse.self) ?? FiltersChangeSetResponse(insert: [], delete: [], revision: revision, replace: false) } - public func getHashPrefixes(revision: Int) async -> HashPrefixResponse { + public func getHashPrefixes(revision: Int) async -> HashPrefixesChangeSetResponse { guard let url = createURL(for: .hashPrefix, revision: revision) else { logDebug("🔸 Invalid hashPrefix revision URL: \(revision)") - return HashPrefixResponse(insert: [], delete: [], revision: revision, replace: false) + return HashPrefixesChangeSetResponse(insert: [], delete: [], revision: revision, replace: false) } - return await fetch(url: url, responseType: HashPrefixResponse.self) ?? HashPrefixResponse(insert: [], delete: [], revision: revision, replace: false) + return await fetch(url: url, responseType: HashPrefixesChangeSetResponse.self) ?? HashPrefixesChangeSetResponse(insert: [], delete: [], revision: revision, replace: false) } public func getMatches(hashPrefix: String) async -> [Match] { @@ -140,10 +109,10 @@ public class PhishingDetectionAPIClient: PhishingDetectionClientProtocol { } // MARK: Private Methods -extension PhishingDetectionAPIClient { +extension APIClient { private func logDebug(_ message: String) { - Logger.phishingDetectionClient.debug("\(message)") + Logger.api.debug("\(message)") } private func createURL(for path: Constants.APIPath, revision: Int? = nil, queryItems: [URLQueryItem]? = nil) -> URL? { diff --git a/Sources/MaliciousSiteProtection/API/ChangeSetResponse.swift b/Sources/MaliciousSiteProtection/API/ChangeSetResponse.swift new file mode 100644 index 000000000..732411895 --- /dev/null +++ b/Sources/MaliciousSiteProtection/API/ChangeSetResponse.swift @@ -0,0 +1,40 @@ +// +// ChangeSetResponse.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 + +extension APIClient { + + public struct ChangeSetResponse: Codable, Equatable { + let insert: [T] + let delete: [T] + let revision: Int + let replace: Bool + + public init(insert: [T], delete: [T], revision: Int, replace: Bool) { + self.insert = insert + self.delete = delete + self.revision = revision + self.replace = replace + } + } + + public typealias FiltersChangeSetResponse = ChangeSetResponse + public typealias HashPrefixesChangeSetResponse = ChangeSetResponse + +} diff --git a/Sources/MaliciousSiteProtection/API/MatchResponse.swift b/Sources/MaliciousSiteProtection/API/MatchResponse.swift new file mode 100644 index 000000000..aaa48b388 --- /dev/null +++ b/Sources/MaliciousSiteProtection/API/MatchResponse.swift @@ -0,0 +1,25 @@ +// +// MatchResponse.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. +// + +extension APIClient { + + public struct MatchResponse: Codable, Equatable { + public var matches: [Match] + } + +} diff --git a/Sources/MaliciousSiteProtection/Logger+MaliciousSiteProtection.swift b/Sources/MaliciousSiteProtection/Logger+MaliciousSiteProtection.swift new file mode 100644 index 000000000..827820401 --- /dev/null +++ b/Sources/MaliciousSiteProtection/Logger+MaliciousSiteProtection.swift @@ -0,0 +1,33 @@ +// +// Logger+MaliciousSiteProtection.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 os + +public extension os.Logger { + struct MaliciousSiteProtection { + public static var general = os.Logger(subsystem: "MSP", category: "General") + public static var api = os.Logger(subsystem: "MSP", category: "API") + public static var dataManager = os.Logger(subsystem: "MSP", category: "DataManager") + public static var updateManager = os.Logger(subsystem: "MSP", category: "UpdateManager") + // TODO: to be dropped + static var phishingDetectionTasks = os.Logger(subsystem: "MSP", category: "BackgroundActivities") + } +} + +internal typealias Logger = os.Logger.MaliciousSiteProtection diff --git a/Sources/PhishingDetection/PhishingDetector.swift b/Sources/MaliciousSiteProtection/MaliciousSiteDetector.swift similarity index 58% rename from Sources/PhishingDetection/PhishingDetector.swift rename to Sources/MaliciousSiteProtection/MaliciousSiteDetector.swift index 3ccbe9b7e..9ac4c01c2 100644 --- a/Sources/PhishingDetection/PhishingDetector.swift +++ b/Sources/MaliciousSiteProtection/MaliciousSiteDetector.swift @@ -1,7 +1,7 @@ // -// PhishingDetector.swift +// MaliciousSiteDetector.swift // -// Copyright © 2023 DuckDuckGo. All rights reserved. +// 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. @@ -16,49 +16,28 @@ // limitations under the License. // -import Foundation -import CryptoKit import Common -import WebKit - -public enum PhishingDetectionError: CustomNSError { - case detected - - public static let errorDomain: String = "PhishingDetectionError" - - public var errorCode: Int { - switch self { - case .detected: - return 1331 - } - } - - public var errorUserInfo: [String: Any] { - switch self { - case .detected: - return [NSLocalizedDescriptionKey: "Phishing detected"] - } - } +import CryptoKit +import Foundation - public var rawValue: Int { - return self.errorCode - } +public protocol MaliciousSiteDetecting { + func evaluate(_ url: URL) async -> ThreatKind? } -public protocol PhishingDetecting { - func isMalicious(url: URL) async -> Bool -} +public final class MaliciousSiteDetector: MaliciousSiteDetecting { + // for easier Xcode symbol navigation + typealias PhishingDetector = MaliciousSiteDetector + typealias MalwareDetector = MaliciousSiteDetector -public class PhishingDetector: PhishingDetecting { let hashPrefixStoreLength: Int = 8 let hashPrefixParamLength: Int = 4 - let apiClient: PhishingDetectionClientProtocol - let dataStore: PhishingDetectionDataSaving - let eventMapping: EventMapping + let apiClient: APIClientProtocol + let dataManager: DataManaging + let eventMapping: EventMapping - public init(apiClient: PhishingDetectionClientProtocol, dataStore: PhishingDetectionDataSaving, eventMapping: EventMapping) { + public init(apiClient: APIClientProtocol = APIClient(), dataManager: DataManaging, eventMapping: EventMapping) { self.apiClient = apiClient - self.dataStore = dataStore + self.dataManager = dataManager self.eventMapping = eventMapping } @@ -67,7 +46,7 @@ public class PhishingDetector: PhishingDetecting { } private func inFilterSet(hash: String) -> Set { - return Set(dataStore.filterSet.filter { $0.hashValue == hash }) + return Set(dataManager.filterSet.filter { $0.hash == hash }) } private func matchesUrl(hash: String, regexPattern: String, url: URL, hostnameHash: String) -> Bool { @@ -92,8 +71,8 @@ public class PhishingDetector: PhishingDetecting { private func checkLocalFilters(canonicalHost: String, canonicalUrl: URL) -> Bool { let hostnameHash = generateHashPrefix(for: canonicalHost, length: Int.max) let filterHit = inFilterSet(hash: hostnameHash) - for filter in filterHit where matchesUrl(hash: filter.hashValue, regexPattern: filter.regex, url: canonicalUrl, hostnameHash: hostnameHash) { - eventMapping.fire(PhishingDetectionEvents.errorPageShown(clientSideHit: true)) + for filter in filterHit where matchesUrl(hash: filter.hash, regexPattern: filter.regex, url: canonicalUrl, hostnameHash: hostnameHash) { + eventMapping.fire(.errorPageShown(clientSideHit: true)) return true } return false @@ -104,27 +83,29 @@ public class PhishingDetector: PhishingDetecting { let matches = await fetchMatches(hashPrefix: hashPrefixParam) let hostnameHash = generateHashPrefix(for: canonicalHost, length: Int.max) for match in matches where matchesUrl(hash: match.hash, regexPattern: match.regex, url: canonicalUrl, hostnameHash: hostnameHash) { - eventMapping.fire(PhishingDetectionEvents.errorPageShown(clientSideHit: false)) + eventMapping.fire(.errorPageShown(clientSideHit: false)) return true } return false } - public func isMalicious(url: URL) async -> Bool { - guard let canonicalHost = url.canonicalHost(), let canonicalUrl = url.canonicalURL() else { return false } - - let hashPrefix = generateHashPrefix(for: canonicalHost, length: hashPrefixStoreLength) - if dataStore.hashPrefixes.contains(hashPrefix) { - // Check local filterSet first - if checkLocalFilters(canonicalHost: canonicalHost, canonicalUrl: canonicalUrl) { - return true - } - // If nothing found, hit the API to get matches - if await checkApiMatches(canonicalHost: canonicalHost, canonicalUrl: canonicalUrl) { - return true + public func evaluate(_ url: URL) async -> ThreatKind? { + guard let canonicalHost = url.canonicalHost(), let canonicalUrl = url.canonicalURL() else { return .none } + + for threatKind in ThreatKind.allCases { + let hashPrefix = generateHashPrefix(for: canonicalHost, length: hashPrefixStoreLength) + if dataManager.hashPrefixes.contains(hashPrefix) { + // Check local filterSet first + if checkLocalFilters(canonicalHost: canonicalHost, canonicalUrl: canonicalUrl) { + return threatKind + } + // If nothing found, hit the API to get matches + if await checkApiMatches(canonicalHost: canonicalHost, canonicalUrl: canonicalUrl) { + return threatKind + } } } - return false + return .none } } diff --git a/Sources/PhishingDetection/PhishingDetectionEvents.swift b/Sources/MaliciousSiteProtection/Model/Event.swift similarity index 96% rename from Sources/PhishingDetection/PhishingDetectionEvents.swift rename to Sources/MaliciousSiteProtection/Model/Event.swift index a788e09ff..31eab462a 100644 --- a/Sources/PhishingDetection/PhishingDetectionEvents.swift +++ b/Sources/MaliciousSiteProtection/Model/Event.swift @@ -1,5 +1,5 @@ // -// PhishingDetectionEvents.swift +// Event.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -26,7 +26,7 @@ public extension PixelKit { } } -public enum PhishingDetectionEvents: PixelKitEventV2 { +public enum Event: PixelKitEventV2 { case errorPageShown(clientSideHit: Bool) case visitSite case iframeLoaded diff --git a/Sources/MaliciousSiteProtection/Model/Filter.swift b/Sources/MaliciousSiteProtection/Model/Filter.swift new file mode 100644 index 000000000..674a176e0 --- /dev/null +++ b/Sources/MaliciousSiteProtection/Model/Filter.swift @@ -0,0 +1,34 @@ +// +// Filter.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 struct Filter: Codable, Hashable { + public var hash: String + public var regex: String + + enum CodingKeys: String, CodingKey { + case hash + case regex + } + + public init(hash: String, regex: String) { + self.hash = hash + self.regex = regex + } +} diff --git a/Sources/MaliciousSiteProtection/Model/MaliciousSiteError.swift b/Sources/MaliciousSiteProtection/Model/MaliciousSiteError.swift new file mode 100644 index 000000000..0cafd5d0d --- /dev/null +++ b/Sources/MaliciousSiteProtection/Model/MaliciousSiteError.swift @@ -0,0 +1,87 @@ +// +// MaliciousSiteError.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 struct MaliciousSiteError: Error, Equatable { + + public enum Code: Int { + case phishing = 1 + case malware = 2 + } + public let code: Code + public let failingUrl: URL + + public init(code: Code, failingUrl: URL) { + self.code = code + self.failingUrl = failingUrl + } + + public init(threat: ThreatKind, failingUrl: URL) { + let code: Code + switch threat { + case .phishing: + code = .phishing + // case .malware: + // code = .malware + } + self.init(code: code, failingUrl: failingUrl) + } + +} + +extension MaliciousSiteError: _ObjectiveCBridgeableError { + + public init?(_bridgedNSError error: NSError) { + guard error.domain == MaliciousSiteError.errorDomain, + let code = Code(rawValue: error.code), + let failingUrl = error.userInfo[NSURLErrorFailingURLErrorKey] as? URL else { return nil } + self.code = code + self.failingUrl = failingUrl + } + +} + +extension MaliciousSiteError: LocalizedError { + + public var errorDescription: String? { + switch code { + case .phishing: + return "Phishing detected" + case .malware: + return "Malware detected" + } + } + +} + +extension MaliciousSiteError: CustomNSError { + public static let errorDomain: String = "MaliciousSiteError" + + public var errorCode: Int { + code.rawValue + } + + public var errorUserInfo: [String: Any] { + [ + NSURLErrorFailingURLErrorKey: failingUrl, + NSLocalizedDescriptionKey: errorDescription! + ] + } + +} diff --git a/Sources/MaliciousSiteProtection/Model/Match.swift b/Sources/MaliciousSiteProtection/Model/Match.swift new file mode 100644 index 000000000..e22cb597f --- /dev/null +++ b/Sources/MaliciousSiteProtection/Model/Match.swift @@ -0,0 +1,35 @@ +// +// Match.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 struct Match: Codable, Hashable { + var hostname: String + var url: String + var regex: String + var hash: String + let category: String? + + public init(hostname: String, url: String, regex: String, hash: String, category: String?) { + self.hostname = hostname + self.url = url + self.regex = regex + self.hash = hash + self.category = category + } +} diff --git a/Sources/MaliciousSiteProtection/Model/ThreatKind.swift b/Sources/MaliciousSiteProtection/Model/ThreatKind.swift new file mode 100644 index 000000000..e77fd5be7 --- /dev/null +++ b/Sources/MaliciousSiteProtection/Model/ThreatKind.swift @@ -0,0 +1,39 @@ +// +// ThreatKind.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 SpecialErrorPages + +public enum ThreatKind: String, CaseIterable, CustomStringConvertible { + public var description: String { rawValue } + + case phishing + // case malware + +} + +public extension ThreatKind { + + var errorPageType: SpecialErrorKind { + switch self { + // case .malware: .malware + case .phishing: .phishing + } + } + +} diff --git a/Sources/PhishingDetection/PhishingDetectionDataActivities.swift b/Sources/MaliciousSiteProtection/PhishingDetectionDataActivities.swift similarity index 92% rename from Sources/PhishingDetection/PhishingDetectionDataActivities.swift rename to Sources/MaliciousSiteProtection/PhishingDetectionDataActivities.swift index 3f195d75e..db4d5a66f 100644 --- a/Sources/PhishingDetection/PhishingDetectionDataActivities.swift +++ b/Sources/MaliciousSiteProtection/PhishingDetectionDataActivities.swift @@ -24,7 +24,7 @@ public protocol BackgroundActivityScheduling: Actor { func start() func stop() } - +// TODO: to be dropped actor BackgroundActivityScheduler: BackgroundActivityScheduling { private var task: Task? @@ -71,9 +71,7 @@ public class PhishingDetectionDataActivities: PhishingDetectionDataActivityHandl private var schedulers: [BackgroundActivityScheduler] private var running: Bool = false - var dataProvider: PhishingDetectionDataProviding - - public init(hashPrefixInterval: TimeInterval = 20 * 60, filterSetInterval: TimeInterval = 12 * 60 * 60, phishingDetectionDataProvider: PhishingDetectionDataProviding, updateManager: PhishingDetectionUpdateManaging) { + public init(hashPrefixInterval: TimeInterval = 20 * 60, filterSetInterval: TimeInterval = 12 * 60 * 60, updateManager: UpdateManaging) { let hashPrefixScheduler = BackgroundActivityScheduler( interval: hashPrefixInterval, identifier: "hashPrefixes.update", @@ -85,7 +83,6 @@ public class PhishingDetectionDataActivities: PhishingDetectionDataActivityHandl activity: { await updateManager.updateFilterSet() } ) self.schedulers = [hashPrefixScheduler, filterSetScheduler] - self.dataProvider = phishingDetectionDataProvider } public func start() { diff --git a/Sources/MaliciousSiteProtection/Services/DataManager.swift b/Sources/MaliciousSiteProtection/Services/DataManager.swift new file mode 100644 index 000000000..41b2dba9d --- /dev/null +++ b/Sources/MaliciousSiteProtection/Services/DataManager.swift @@ -0,0 +1,180 @@ +// +// DataManager.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 Common +import os + +public protocol DataManaging { + var filterSet: Set { get } + var hashPrefixes: Set { get } + var currentRevision: Int { get } + func saveFilterSet(set: Set) + func saveHashPrefixes(set: Set) + func saveRevision(_ revision: Int) +} + +public final class DataManager: DataManaging { + private lazy var _filterSet: Set = { + loadFilterSet() + }() + + private lazy var _hashPrefixes: Set = { + loadHashPrefix() + }() + + private lazy var _currentRevision: Int = { + loadRevision() + }() + + public private(set) var filterSet: Set { + get { _filterSet } + set { _filterSet = newValue } + } + public private(set) var hashPrefixes: Set { + get { _hashPrefixes } + set { _hashPrefixes = newValue } + } + public private(set) var currentRevision: Int { + get { _currentRevision } + set { _currentRevision = newValue } + } + + private let embeddedDataProvider: EmbeddedDataProviding + private let fileStore: FileStoring + private let encoder = JSONEncoder() + private let revisionFilename = "revision.txt" + private let hashPrefixFilename = "phishingHashPrefixes.json" + private let filterSetFilename = "phishingFilterSet.json" + + public init(embeddedDataProvider: EmbeddedDataProviding, fileStore: FileStoring? = nil) { + self.embeddedDataProvider = embeddedDataProvider + self.fileStore = fileStore ?? FileStore() + } + + private func writeHashPrefixes() { + let encoder = JSONEncoder() + do { + let hashPrefixesData = try encoder.encode(Array(hashPrefixes)) + fileStore.write(data: hashPrefixesData, to: hashPrefixFilename) + } catch { + Logger.dataManager.error("Error saving hash prefixes data: \(error.localizedDescription)") + } + } + + private func writeFilterSet() { + let encoder = JSONEncoder() + do { + let filterSetData = try encoder.encode(Array(filterSet)) + fileStore.write(data: filterSetData, to: filterSetFilename) + } catch { + Logger.dataManager.error("Error saving filter set data: \(error.localizedDescription)") + } + } + + private func writeRevision() { + let encoder = JSONEncoder() + do { + let revisionData = try encoder.encode(currentRevision) + fileStore.write(data: revisionData, to: revisionFilename) + } catch { + Logger.dataManager.error("Error saving revision data: \(error.localizedDescription)") + } + } + + private func loadHashPrefix() -> Set { + guard let data = fileStore.read(from: hashPrefixFilename) else { + return embeddedDataProvider.loadEmbeddedHashPrefixes() + } + let decoder = JSONDecoder() + do { + if loadRevisionFromDisk() < embeddedDataProvider.embeddedRevision { + return embeddedDataProvider.loadEmbeddedHashPrefixes() + } + let onDiskHashPrefixes = Set(try decoder.decode(Set.self, from: data)) + return onDiskHashPrefixes + } catch { + Logger.dataManager.error("Error decoding \(self.hashPrefixFilename): \(error.localizedDescription)") + return embeddedDataProvider.loadEmbeddedHashPrefixes() + } + } + + private func loadFilterSet() -> Set { + guard let data = fileStore.read(from: filterSetFilename) else { + return embeddedDataProvider.loadEmbeddedFilterSet() + } + let decoder = JSONDecoder() + do { + if loadRevisionFromDisk() < embeddedDataProvider.embeddedRevision { + return embeddedDataProvider.loadEmbeddedFilterSet() + } + let onDiskFilterSet = Set(try decoder.decode(Set.self, from: data)) + return onDiskFilterSet + } catch { + Logger.dataManager.error("Error decoding \(self.filterSetFilename): \(error.localizedDescription)") + return embeddedDataProvider.loadEmbeddedFilterSet() + } + } + + private func loadRevisionFromDisk() -> Int { + guard let data = fileStore.read(from: revisionFilename) else { + return embeddedDataProvider.embeddedRevision + } + let decoder = JSONDecoder() + do { + return try decoder.decode(Int.self, from: data) + } catch { + Logger.dataManager.error("Error decoding \(self.revisionFilename): \(error.localizedDescription)") + return embeddedDataProvider.embeddedRevision + } + } + + private func loadRevision() -> Int { + guard let data = fileStore.read(from: revisionFilename) else { + return embeddedDataProvider.embeddedRevision + } + let decoder = JSONDecoder() + do { + let loadedRevision = try decoder.decode(Int.self, from: data) + if loadedRevision < embeddedDataProvider.embeddedRevision { + return embeddedDataProvider.embeddedRevision + } + return loadedRevision + } catch { + Logger.dataManager.error("Error decoding \(self.revisionFilename): \(error.localizedDescription)") + return embeddedDataProvider.embeddedRevision + } + } +} + +extension DataManager { + public func saveFilterSet(set: Set) { + self.filterSet = set + writeFilterSet() + } + + public func saveHashPrefixes(set: Set) { + self.hashPrefixes = set + writeHashPrefixes() + } + + public func saveRevision(_ revision: Int) { + self.currentRevision = revision + writeRevision() + } +} diff --git a/Sources/PhishingDetection/PhishingDetectionDataProvider.swift b/Sources/MaliciousSiteProtection/Services/EmbeddedDataProvider.swift similarity index 76% rename from Sources/PhishingDetection/PhishingDetectionDataProvider.swift rename to Sources/MaliciousSiteProtection/Services/EmbeddedDataProvider.swift index af1c87672..5ca4d9f7c 100644 --- a/Sources/PhishingDetection/PhishingDetectionDataProvider.swift +++ b/Sources/MaliciousSiteProtection/Services/EmbeddedDataProvider.swift @@ -1,5 +1,5 @@ // -// PhishingDetectionDataProvider.swift +// EmbeddedDataProvider.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -21,18 +21,18 @@ import CryptoKit import Common import os -public protocol PhishingDetectionDataProviding { +public protocol EmbeddedDataProviding { var embeddedRevision: Int { get } func loadEmbeddedFilterSet() -> Set func loadEmbeddedHashPrefixes() -> Set } -public class PhishingDetectionDataProvider: PhishingDetectionDataProviding { - public private(set) var embeddedRevision: Int - var embeddedFilterSetURL: URL - var embeddedFilterSetDataSHA: String - var embeddedHashPrefixURL: URL - var embeddedHashPrefixDataSHA: String +public struct EmbeddedDataProvider: EmbeddedDataProviding { + public let embeddedRevision: Int + private let embeddedFilterSetURL: URL + private let embeddedFilterSetDataSHA: String + private let embeddedHashPrefixURL: URL + private let embeddedHashPrefixDataSHA: String public init(revision: Int, filterSetURL: URL, filterSetDataSHA: String, hashPrefixURL: URL, hashPrefixDataSHA: String) { embeddedFilterSetURL = filterSetURL @@ -58,8 +58,7 @@ public class PhishingDetectionDataProvider: PhishingDetectionDataProviding { let filterSetData = try loadData(from: embeddedFilterSetURL, expectedSHA: embeddedFilterSetDataSHA) return try JSONDecoder().decode(Set.self, from: filterSetData) } catch { - Logger.phishingDetectionDataProvider.error("🔴 Error: SHA mismatch for filterSet JSON file. Expected \(self.embeddedFilterSetDataSHA)") - return [] + fatalError("🔴 Error: SHA mismatch for filterSet JSON file. Expected \(self.embeddedFilterSetDataSHA)") } } @@ -68,8 +67,7 @@ public class PhishingDetectionDataProvider: PhishingDetectionDataProviding { let hashPrefixData = try loadData(from: embeddedHashPrefixURL, expectedSHA: embeddedHashPrefixDataSHA) return try JSONDecoder().decode(Set.self, from: hashPrefixData) } catch { - Logger.phishingDetectionDataProvider.error("🔴 Error: SHA mismatch for hashPrefixes JSON file. Expected \(self.embeddedHashPrefixDataSHA)") - return [] + fatalError("🔴 Error: SHA mismatch for hashPrefixes JSON file. Expected \(self.embeddedHashPrefixDataSHA)") } } } diff --git a/Sources/MaliciousSiteProtection/Services/FileStore.swift b/Sources/MaliciousSiteProtection/Services/FileStore.swift new file mode 100644 index 000000000..e0714401a --- /dev/null +++ b/Sources/MaliciousSiteProtection/Services/FileStore.swift @@ -0,0 +1,68 @@ +// +// FileStore.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 os + +public protocol FileStoring { + func write(data: Data, to filename: String) + func read(from filename: String) -> Data? +} + +public struct FileStore: FileStoring { + private let dataStoreURL: URL + + public init() { + let dataStoreDirectory: URL + do { + dataStoreDirectory = try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + } catch { + Logger.dataManager.error("Error accessing application support directory: \(error.localizedDescription)") + dataStoreDirectory = FileManager.default.temporaryDirectory + } + dataStoreURL = dataStoreDirectory.appendingPathComponent(Bundle.main.bundleIdentifier!, isDirectory: true) + createDirectoryIfNeeded() + } + + private func createDirectoryIfNeeded() { + do { + try FileManager.default.createDirectory(at: dataStoreURL, withIntermediateDirectories: true, attributes: nil) + } catch { + Logger.dataManager.error("Failed to create directory: \(error.localizedDescription)") + } + } + + public func write(data: Data, to filename: String) { + let fileURL = dataStoreURL.appendingPathComponent(filename) + do { + try data.write(to: fileURL) + } catch { + Logger.dataManager.error("Error writing to directory: \(error.localizedDescription)") + } + } + + public func read(from filename: String) -> Data? { + let fileURL = dataStoreURL.appendingPathComponent(filename) + do { + return try Data(contentsOf: fileURL) + } catch { + Logger.dataManager.error("Error accessing application support directory: \(error)") + return nil + } + } +} diff --git a/Sources/PhishingDetection/PhishingDetectionUpdateManager.swift b/Sources/MaliciousSiteProtection/Services/UpdateManager.swift similarity index 60% rename from Sources/PhishingDetection/PhishingDetectionUpdateManager.swift rename to Sources/MaliciousSiteProtection/Services/UpdateManager.swift index b811082e3..053acd230 100644 --- a/Sources/PhishingDetection/PhishingDetectionUpdateManager.swift +++ b/Sources/MaliciousSiteProtection/Services/UpdateManager.swift @@ -1,7 +1,7 @@ // -// PhishingDetectionUpdateManager.swift +// UpdateManager.swift // -// Copyright © 2023 DuckDuckGo. All rights reserved. +// 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. @@ -20,18 +20,18 @@ import Foundation import Common import os -public protocol PhishingDetectionUpdateManaging { +public protocol UpdateManaging { func updateFilterSet() async func updateHashPrefixes() async } -public class PhishingDetectionUpdateManager: PhishingDetectionUpdateManaging { - var apiClient: PhishingDetectionClientProtocol - var dataStore: PhishingDetectionDataSaving +public struct UpdateManager: UpdateManaging { + private let apiClient: APIClientProtocol + private let dataManager: DataManaging - public init(client: PhishingDetectionClientProtocol, dataStore: PhishingDetectionDataSaving) { - self.apiClient = client - self.dataStore = dataStore + public init(apiClient: APIClientProtocol, dataManager: DataManaging) { + self.apiClient = apiClient + self.dataManager = dataManager } private func updateSet( @@ -54,30 +54,30 @@ public class PhishingDetectionUpdateManager: PhishingDetectionUpdateManaging { } public func updateFilterSet() async { - let response = await apiClient.getFilterSet(revision: dataStore.currentRevision) + let response = await apiClient.getFilterSet(revision: dataManager.currentRevision) updateSet( - currentSet: dataStore.filterSet, + currentSet: dataManager.filterSet, insert: response.insert, delete: response.delete, replace: response.replace ) { newSet in - self.dataStore.saveFilterSet(set: newSet) + self.dataManager.saveFilterSet(set: newSet) } - dataStore.saveRevision(response.revision) - Logger.phishingDetectionUpdateManager.debug("filterSet updated to revision \(self.dataStore.currentRevision)") + dataManager.saveRevision(response.revision) + Logger.updateManager.debug("filterSet updated to revision \(self.dataManager.currentRevision)") } public func updateHashPrefixes() async { - let response = await apiClient.getHashPrefixes(revision: dataStore.currentRevision) + let response = await apiClient.getHashPrefixes(revision: dataManager.currentRevision) updateSet( - currentSet: dataStore.hashPrefixes, + currentSet: dataManager.hashPrefixes, insert: response.insert, delete: response.delete, replace: response.replace ) { newSet in - self.dataStore.saveHashPrefixes(set: newSet) + self.dataManager.saveHashPrefixes(set: newSet) } - dataStore.saveRevision(response.revision) - Logger.phishingDetectionUpdateManager.debug("hashPrefixes updated to revision \(self.dataStore.currentRevision)") + dataManager.saveRevision(response.revision) + Logger.updateManager.debug("hashPrefixes updated to revision \(self.dataManager.currentRevision)") } } diff --git a/Sources/Navigation/Extensions/WKErrorExtension.swift b/Sources/Navigation/Extensions/WKErrorExtension.swift index f1a5c238d..484d9dd62 100644 --- a/Sources/Navigation/Extensions/WKErrorExtension.swift +++ b/Sources/Navigation/Extensions/WKErrorExtension.swift @@ -33,6 +33,10 @@ extension WKError { code.rawValue == NSURLErrorCancelled && _nsError.domain == NSURLErrorDomain } + public var isServerCertificateUntrusted: Bool { + code.rawValue == NSURLErrorServerCertificateUntrusted && _nsError.domain == NSURLErrorDomain + } + } extension WKError { diff --git a/Sources/Networking/v1/APIHeaders.swift b/Sources/Networking/v1/APIHeaders.swift index 6d7f0a4b0..a5786c949 100644 --- a/Sources/Networking/v1/APIHeaders.swift +++ b/Sources/Networking/v1/APIHeaders.swift @@ -25,7 +25,7 @@ public extension APIRequest { struct Headers { public typealias UserAgent = String - private static var userAgent: UserAgent? + public private(set) static var userAgent: UserAgent? public static func setUserAgent(_ userAgent: UserAgent) { self.userAgent = userAgent } diff --git a/Sources/Onboarding/ContextualDaxDialogs/OnboardingSuggestionsViewModel.swift b/Sources/Onboarding/ContextualDaxDialogs/OnboardingSuggestionsViewModel.swift index d10fecd56..ffff91188 100644 --- a/Sources/Onboarding/ContextualDaxDialogs/OnboardingSuggestionsViewModel.swift +++ b/Sources/Onboarding/ContextualDaxDialogs/OnboardingSuggestionsViewModel.swift @@ -19,8 +19,8 @@ import Foundation public protocol OnboardingNavigationDelegate: AnyObject { - func searchFor(_ query: String) - func navigateTo(url: URL) + func searchFromOnboarding(for query: String) + func navigateFromOnboarding(to url: URL) } public protocol OnboardingSearchSuggestionsPixelReporting { @@ -52,7 +52,7 @@ public struct OnboardingSearchSuggestionsViewModel { public func listItemPressed(_ item: ContextualOnboardingListItem) { pixelReporter.trackSearchSuggetionOptionTapped() - delegate?.searchFor(item.title) + delegate?.searchFromOnboarding(for: item.title) } } @@ -82,6 +82,6 @@ public struct OnboardingSiteSuggestionsViewModel { public func listItemPressed(_ item: ContextualOnboardingListItem) { guard let url = URL(string: item.title) else { return } pixelReporter.trackSiteSuggetionOptionTapped() - delegate?.navigateTo(url: url) + delegate?.navigateFromOnboarding(to: url) } } diff --git a/Sources/PhishingDetection/Logger+PhishingDetection.swift b/Sources/PhishingDetection/Logger+PhishingDetection.swift deleted file mode 100644 index 96a606772..000000000 --- a/Sources/PhishingDetection/Logger+PhishingDetection.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Logger+PhishingDetection.swift -// -// Copyright © 2023 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 os - -public extension Logger { - static var phishingDetection: Logger = { Logger(subsystem: "Phishing Detection", category: "") }() - static var phishingDetectionClient: Logger = { Logger(subsystem: "Phishing Detection", category: "APIClient") }() - static var phishingDetectionTasks: Logger = { Logger(subsystem: "Phishing Detection", category: "BackgroundActivities") }() - static var phishingDetectionDataProvider: Logger = { Logger(subsystem: "Phishing Detection", category: "DataProvider") }() - static var phishingDetectionDataStore: Logger = { Logger(subsystem: "Phishing Detection", category: "DataStore") }() - static var phishingDetectionUpdateManager: Logger = { Logger(subsystem: "Phishing Detection", category: "UpdateManager") }() -} diff --git a/Sources/PhishingDetection/PhishingDetectionDataStore.swift b/Sources/PhishingDetection/PhishingDetectionDataStore.swift deleted file mode 100644 index f247f90b8..000000000 --- a/Sources/PhishingDetection/PhishingDetectionDataStore.swift +++ /dev/null @@ -1,266 +0,0 @@ -// -// PhishingDetectionDataStore.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 Common -import os - -enum PhishingDetectionDataError: Error { - case empty -} - -public struct Filter: Codable, Hashable { - public var hashValue: String - public var regex: String - - enum CodingKeys: String, CodingKey { - case hashValue = "hash" - case regex - } - - public init(hashValue: String, regex: String) { - self.hashValue = hashValue - self.regex = regex - } -} - -public struct Match: Codable, Hashable { - var hostname: String - var url: String - var regex: String - var hash: String - - public init(hostname: String, url: String, regex: String, hash: String) { - self.hostname = hostname - self.url = url - self.regex = regex - self.hash = hash - } -} - -public protocol PhishingDetectionDataSaving { - var filterSet: Set { get } - var hashPrefixes: Set { get } - var currentRevision: Int { get } - func saveFilterSet(set: Set) - func saveHashPrefixes(set: Set) - func saveRevision(_ revision: Int) -} - -public class PhishingDetectionDataStore: PhishingDetectionDataSaving { - private lazy var _filterSet: Set = { - loadFilterSet() - }() - - private lazy var _hashPrefixes: Set = { - loadHashPrefix() - }() - - private lazy var _currentRevision: Int = { - loadRevision() - }() - - public private(set) var filterSet: Set { - get { _filterSet } - set { _filterSet = newValue } - } - public private(set) var hashPrefixes: Set { - get { _hashPrefixes } - set { _hashPrefixes = newValue } - } - public private(set) var currentRevision: Int { - get { _currentRevision } - set { _currentRevision = newValue } - } - - private let dataProvider: PhishingDetectionDataProviding - private let fileStorageManager: FileStorageManager - private let encoder = JSONEncoder() - private let revisionFilename = "revision.txt" - private let hashPrefixFilename = "hashPrefixes.json" - private let filterSetFilename = "filterSet.json" - - public init(dataProvider: PhishingDetectionDataProviding, - fileStorageManager: FileStorageManager? = nil) { - self.dataProvider = dataProvider - if let injectedFileStorageManager = fileStorageManager { - self.fileStorageManager = injectedFileStorageManager - } else { - self.fileStorageManager = PhishingFileStorageManager() - } - } - - private func writeHashPrefixes() { - let encoder = JSONEncoder() - do { - let hashPrefixesData = try encoder.encode(Array(hashPrefixes)) - fileStorageManager.write(data: hashPrefixesData, to: hashPrefixFilename) - } catch { - Logger.phishingDetectionDataStore.error("Error saving hash prefixes data: \(error.localizedDescription)") - } - } - - private func writeFilterSet() { - let encoder = JSONEncoder() - do { - let filterSetData = try encoder.encode(Array(filterSet)) - fileStorageManager.write(data: filterSetData, to: filterSetFilename) - } catch { - Logger.phishingDetectionDataStore.error("Error saving filter set data: \(error.localizedDescription)") - } - } - - private func writeRevision() { - let encoder = JSONEncoder() - do { - let revisionData = try encoder.encode(currentRevision) - fileStorageManager.write(data: revisionData, to: revisionFilename) - } catch { - Logger.phishingDetectionDataStore.error("Error saving revision data: \(error.localizedDescription)") - } - } - - private func loadHashPrefix() -> Set { - guard let data = fileStorageManager.read(from: hashPrefixFilename) else { - return dataProvider.loadEmbeddedHashPrefixes() - } - let decoder = JSONDecoder() - do { - if loadRevisionFromDisk() < dataProvider.embeddedRevision { - return dataProvider.loadEmbeddedHashPrefixes() - } - let onDiskHashPrefixes = Set(try decoder.decode(Set.self, from: data)) - return onDiskHashPrefixes - } catch { - Logger.phishingDetectionDataStore.error("Error decoding \(self.hashPrefixFilename): \(error.localizedDescription)") - return dataProvider.loadEmbeddedHashPrefixes() - } - } - - private func loadFilterSet() -> Set { - guard let data = fileStorageManager.read(from: filterSetFilename) else { - return dataProvider.loadEmbeddedFilterSet() - } - let decoder = JSONDecoder() - do { - if loadRevisionFromDisk() < dataProvider.embeddedRevision { - return dataProvider.loadEmbeddedFilterSet() - } - let onDiskFilterSet = Set(try decoder.decode(Set.self, from: data)) - return onDiskFilterSet - } catch { - Logger.phishingDetectionDataStore.error("Error decoding \(self.filterSetFilename): \(error.localizedDescription)") - return dataProvider.loadEmbeddedFilterSet() - } - } - - private func loadRevisionFromDisk() -> Int { - guard let data = fileStorageManager.read(from: revisionFilename) else { - return dataProvider.embeddedRevision - } - let decoder = JSONDecoder() - do { - return try decoder.decode(Int.self, from: data) - } catch { - Logger.phishingDetectionDataStore.error("Error decoding \(self.revisionFilename): \(error.localizedDescription)") - return dataProvider.embeddedRevision - } - } - - private func loadRevision() -> Int { - guard let data = fileStorageManager.read(from: revisionFilename) else { - return dataProvider.embeddedRevision - } - let decoder = JSONDecoder() - do { - let loadedRevision = try decoder.decode(Int.self, from: data) - if loadedRevision < dataProvider.embeddedRevision { - return dataProvider.embeddedRevision - } - return loadedRevision - } catch { - Logger.phishingDetectionDataStore.error("Error decoding \(self.revisionFilename): \(error.localizedDescription)") - return dataProvider.embeddedRevision - } - } -} - -extension PhishingDetectionDataStore { - public func saveFilterSet(set: Set) { - self.filterSet = set - writeFilterSet() - } - - public func saveHashPrefixes(set: Set) { - self.hashPrefixes = set - writeHashPrefixes() - } - - public func saveRevision(_ revision: Int) { - self.currentRevision = revision - writeRevision() - } -} - -public protocol FileStorageManager { - func write(data: Data, to filename: String) - func read(from filename: String) -> Data? -} - -final class PhishingFileStorageManager: FileStorageManager { - private let dataStoreURL: URL - - init() { - let dataStoreDirectory: URL - do { - dataStoreDirectory = try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) - } catch { - Logger.phishingDetectionDataStore.error("Error accessing application support directory: \(error.localizedDescription)") - dataStoreDirectory = FileManager.default.temporaryDirectory - } - dataStoreURL = dataStoreDirectory.appendingPathComponent(Bundle.main.bundleIdentifier!, isDirectory: true) - createDirectoryIfNeeded() - } - - private func createDirectoryIfNeeded() { - do { - try FileManager.default.createDirectory(at: dataStoreURL, withIntermediateDirectories: true, attributes: nil) - } catch { - Logger.phishingDetectionDataStore.error("Failed to create directory: \(error.localizedDescription)") - } - } - - func write(data: Data, to filename: String) { - let fileURL = dataStoreURL.appendingPathComponent(filename) - do { - try data.write(to: fileURL) - } catch { - Logger.phishingDetectionDataStore.error("Error writing to directory: \(error.localizedDescription)") - } - } - - func read(from filename: String) -> Data? { - let fileURL = dataStoreURL.appendingPathComponent(filename) - do { - return try Data(contentsOf: fileURL) - } catch { - Logger.phishingDetectionDataStore.error("Error accessing application support directory: \(error)") - return nil - } - } -} diff --git a/Sources/PrivacyDashboard/PrivacyDashboardController.swift b/Sources/PrivacyDashboard/PrivacyDashboardController.swift index 8093a02d2..1e9c0b297 100644 --- a/Sources/PrivacyDashboard/PrivacyDashboardController.swift +++ b/Sources/PrivacyDashboard/PrivacyDashboardController.swift @@ -16,12 +16,13 @@ // limitations under the License. // -import Foundation -import WebKit -import Combine -import PrivacyDashboardResources import BrowserServicesKit +import Combine import Common +import Foundation +import MaliciousSiteProtection +import PrivacyDashboardResources +import WebKit public enum PrivacyDashboardOpenSettingsTarget: String { @@ -205,7 +206,7 @@ extension PrivacyDashboardController: WKNavigationDelegate { subscribeToServerTrust() subscribeToConsentManaged() subscribeToAllowedPermissions() - subscribeToIsPhishing() + subscribeToMaliciousSiteThreatKind() } private func subscribeToTheme() { @@ -259,12 +260,17 @@ extension PrivacyDashboardController: WKNavigationDelegate { .store(in: &cancellables) } - private func subscribeToIsPhishing() { - privacyInfo?.$isPhishing + private func subscribeToMaliciousSiteThreatKind() { + privacyInfo?.$malicousSiteThreatKind .receive(on: DispatchQueue.main ) - .sink(receiveValue: { [weak self] isPhishing in - guard let self = self, let webView = self.webView else { return } - script.setIsPhishing(isPhishing, webView: webView) + .sink(receiveValue: { [weak self] detectedThreatKind in + guard let self, let webView else { return } + for threatKind in MaliciousSiteProtection.ThreatKind.allCases { + switch threatKind { + case .phishing: + script.setIsPhishing(detectedThreatKind == threatKind, webView: webView) + } + } }) .store(in: &cancellables) } diff --git a/Sources/PrivacyDashboard/PrivacyInfo.swift b/Sources/PrivacyDashboard/PrivacyInfo.swift index b9db906fc..3eaabc185 100644 --- a/Sources/PrivacyDashboard/PrivacyInfo.swift +++ b/Sources/PrivacyDashboard/PrivacyInfo.swift @@ -16,9 +16,10 @@ // limitations under the License. // +import Common import Foundation +import MaliciousSiteProtection import TrackerRadarKit -import Common public protocol SecurityTrust { } extension SecTrust: SecurityTrust {} @@ -33,15 +34,15 @@ public final class PrivacyInfo { @Published public var serverTrust: SecurityTrust? @Published public var connectionUpgradedTo: URL? @Published public var cookieConsentManaged: CookieConsentInfo? - @Published public var isPhishing: Bool + @Published public var malicousSiteThreatKind: MaliciousSiteProtection.ThreatKind? @Published public var isSpecialErrorPageVisible: Bool = false @Published public var shouldCheckServerTrust: Bool - public init(url: URL, parentEntity: Entity?, protectionStatus: ProtectionStatus, isPhishing: Bool = false, shouldCheckServerTrust: Bool = false) { + public init(url: URL, parentEntity: Entity?, protectionStatus: ProtectionStatus, malicousSiteThreatKind: MaliciousSiteProtection.ThreatKind? = .none, shouldCheckServerTrust: Bool = false) { self.url = url self.parentEntity = parentEntity self.protectionStatus = protectionStatus - self.isPhishing = isPhishing + self.malicousSiteThreatKind = malicousSiteThreatKind self.shouldCheckServerTrust = shouldCheckServerTrust trackerInfo = TrackerInfo() diff --git a/Sources/UserScript/UserScript.swift b/Sources/UserScript/UserScript.swift index 3b35ddc42..728b3b36a 100644 --- a/Sources/UserScript/UserScript.swift +++ b/Sources/UserScript/UserScript.swift @@ -107,7 +107,7 @@ extension UserScript { } public func makeWKUserScript() async -> WKUserScriptBox { - let source = (try? await Task.detached { [source] in Self.prepareScriptSource(from: source) }.result.get())! + let source = await Task.detached { [source] in Self.prepareScriptSource(from: source) }.result.get() return await Self.makeWKUserScript(from: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, diff --git a/Tests/PhishingDetectionTests/BackgroundActivitySchedulerTests.swift b/Tests/MaliciousSiteProtectionTests/BackgroundActivitySchedulerTests.swift similarity index 97% rename from Tests/PhishingDetectionTests/BackgroundActivitySchedulerTests.swift rename to Tests/MaliciousSiteProtectionTests/BackgroundActivitySchedulerTests.swift index 8d907efbd..0640fc16f 100644 --- a/Tests/PhishingDetectionTests/BackgroundActivitySchedulerTests.swift +++ b/Tests/MaliciousSiteProtectionTests/BackgroundActivitySchedulerTests.swift @@ -17,7 +17,7 @@ // import Foundation import XCTest -@testable import PhishingDetection +@testable import MaliciousSiteProtection class BackgroundActivitySchedulerTests: XCTestCase { var scheduler: BackgroundActivityScheduler! diff --git a/Tests/MaliciousSiteProtectionTests/MaliciousSiteDetectorTests.swift b/Tests/MaliciousSiteProtectionTests/MaliciousSiteDetectorTests.swift new file mode 100644 index 000000000..dd633be54 --- /dev/null +++ b/Tests/MaliciousSiteProtectionTests/MaliciousSiteDetectorTests.swift @@ -0,0 +1,105 @@ +// +// MaliciousSiteDetectorTests.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 XCTest + +@testable import MaliciousSiteProtection + +class MaliciousSiteDetectorTests: XCTestCase { + + private var mockAPIClient: MockMaliciousSiteProtectionAPIClient! + private var mockDataManager: MockMaliciousSiteProtectionDataManager! + private var mockEventMapping: MockEventMapping! + private var detector: MaliciousSiteDetector! + + override func setUp() { + super.setUp() + mockAPIClient = MockMaliciousSiteProtectionAPIClient() + mockDataManager = MockMaliciousSiteProtectionDataManager() + mockEventMapping = MockEventMapping() + detector = MaliciousSiteDetector(apiClient: mockAPIClient, dataManager: mockDataManager, eventMapping: mockEventMapping) + } + + override func tearDown() { + mockAPIClient = nil + mockDataManager = nil + mockEventMapping = nil + detector = nil + super.tearDown() + } + + func testIsMaliciousWithLocalFilterHit() async { + let filter = Filter(hash: "255a8a793097aeea1f06a19c08cde28db0eb34c660c6e4e7480c9525d034b16d", regex: ".*malicious.*") + mockDataManager.filterSet = Set([filter]) + mockDataManager.hashPrefixes = Set(["255a8a79"]) + + let url = URL(string: "https://malicious.com/")! + + let result = await detector.evaluate(url) + + XCTAssertEqual(result, .phishing) + } + + func testIsMaliciousWithApiMatch() async { + mockDataManager.filterSet = Set() + mockDataManager.hashPrefixes = ["a379a6f6"] + + let url = URL(string: "https://example.com/mal")! + + let result = await detector.evaluate(url) + + XCTAssertEqual(result, .phishing) + } + + func testIsMaliciousWithHashPrefixMatch() async { + let filter = Filter(hash: "notamatch", regex: ".*malicious.*") + mockDataManager.filterSet = [filter] + mockDataManager.hashPrefixes = ["4c64eb24"] // matches safe.com + + let url = URL(string: "https://safe.com")! + + let result = await detector.evaluate(url) + + XCTAssertNil(result) + } + + func testIsMaliciousWithFullHashMatch() async { + // 4c64eb2468bcd3e113b37167e6b819aeccf550f974a6082ef17fb74ca68e823b + let filter = Filter(hash: "4c64eb2468bcd3e113b37167e6b819aeccf550f974a6082ef17fb74ca68e823b", regex: "https://safe.com/maliciousURI") + mockDataManager.filterSet = [filter] + mockDataManager.hashPrefixes = ["4c64eb24"] + + let url = URL(string: "https://safe.com")! + + let result = await detector.evaluate(url) + + XCTAssertNil(result) + } + + func testIsMaliciousWithNoHashPrefixMatch() async { + let filter = Filter(hash: "testHash", regex: ".*malicious.*") + mockDataManager.filterSet = [filter] + mockDataManager.hashPrefixes = ["testPrefix"] + + let url = URL(string: "https://safe.com")! + + let result = await detector.evaluate(url) + + XCTAssertNil(result) + } +} diff --git a/Tests/PhishingDetectionTests/PhishingDetectionClientTests.swift b/Tests/MaliciousSiteProtectionTests/MaliciousSiteProtectionAPIClientTests.swift similarity index 71% rename from Tests/PhishingDetectionTests/PhishingDetectionClientTests.swift rename to Tests/MaliciousSiteProtectionTests/MaliciousSiteProtectionAPIClientTests.swift index 6826c86d6..52f74b97c 100644 --- a/Tests/PhishingDetectionTests/PhishingDetectionClientTests.swift +++ b/Tests/MaliciousSiteProtectionTests/MaliciousSiteProtectionAPIClientTests.swift @@ -1,5 +1,5 @@ // -// PhishingDetectionClientTests.swift +// MaliciousSiteProtectionAPIClientTests.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -17,17 +17,17 @@ // import Foundation import XCTest -@testable import PhishingDetection +@testable import MaliciousSiteProtection -final class PhishingDetectionAPIClientTests: XCTestCase { +final class MaliciousSiteProtectionAPIClientTests: XCTestCase { var mockSession: MockURLSession! - var client: PhishingDetectionAPIClient! + var client: MaliciousSiteProtection.APIClient! override func setUp() { super.setUp() mockSession = MockURLSession() - client = PhishingDetectionAPIClient(environment: .staging, session: mockSession) + client = .init(environment: .staging, session: mockSession) } override func tearDown() { @@ -38,9 +38,9 @@ final class PhishingDetectionAPIClientTests: XCTestCase { func testGetFilterSetSuccess() async { // Given - let insertFilter = Filter(hashValue: "a379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce1947", regex: ".") - let deleteFilter = Filter(hashValue: "6a929cd0b3ba4677eaedf1b2bdaf3ff89281cca94f688c83103bc9a676aea46d", regex: "(?i)^https?\\:\\/\\/[\\w\\-\\.]+(?:\\:(?:80|443))?") - let expectedResponse = FilterSetResponse(insert: [insertFilter], delete: [deleteFilter], revision: 1, replace: false) + let insertFilter = Filter(hash: "a379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce1947", regex: ".") + let deleteFilter = Filter(hash: "6a929cd0b3ba4677eaedf1b2bdaf3ff89281cca94f688c83103bc9a676aea46d", regex: "(?i)^https?\\:\\/\\/[\\w\\-\\.]+(?:\\:(?:80|443))?") + let expectedResponse = APIClient.FiltersChangeSetResponse(insert: [insertFilter], delete: [deleteFilter], revision: 1, replace: false) mockSession.data = try? JSONEncoder().encode(expectedResponse) mockSession.response = HTTPURLResponse(url: client.filterSetURL, statusCode: 200, httpVersion: nil, headerFields: nil) @@ -53,7 +53,7 @@ final class PhishingDetectionAPIClientTests: XCTestCase { func testGetHashPrefixesSuccess() async { // Given - let expectedResponse = HashPrefixResponse(insert: ["abc"], delete: ["def"], revision: 1, replace: false) + let expectedResponse = APIClient.HashPrefixesChangeSetResponse(insert: ["abc"], delete: ["def"], revision: 1, replace: false) mockSession.data = try? JSONEncoder().encode(expectedResponse) mockSession.response = HTTPURLResponse(url: client.hashPrefixURL, statusCode: 200, httpVersion: nil, headerFields: nil) @@ -66,7 +66,7 @@ final class PhishingDetectionAPIClientTests: XCTestCase { func testGetMatchesSuccess() async { // Given - let expectedResponse = MatchResponse(matches: [Match(hostname: "example.com", url: "https://example.com/test", regex: ".", hash: "a379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce1947")]) + let expectedResponse = APIClient.MatchResponse(matches: [Match(hostname: "example.com", url: "https://example.com/test", regex: ".", hash: "a379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce1947", category: nil)]) mockSession.data = try? JSONEncoder().encode(expectedResponse) mockSession.response = HTTPURLResponse(url: client.matchesURL, statusCode: 200, httpVersion: nil, headerFields: nil) @@ -85,7 +85,7 @@ final class PhishingDetectionAPIClientTests: XCTestCase { let response = await client.getFilterSet(revision: invalidRevision) // Then - XCTAssertEqual(response, FilterSetResponse(insert: [], delete: [], revision: invalidRevision, replace: false)) + XCTAssertEqual(response, .init(insert: [], delete: [], revision: invalidRevision, replace: false)) } func testGetHashPrefixesInvalidURL() async { @@ -96,7 +96,7 @@ final class PhishingDetectionAPIClientTests: XCTestCase { let response = await client.getHashPrefixes(revision: invalidRevision) // Then - XCTAssertEqual(response, HashPrefixResponse(insert: [], delete: [], revision: invalidRevision, replace: false)) + XCTAssertEqual(response, .init(insert: [], delete: [], revision: invalidRevision, replace: false)) } func testGetMatchesInvalidURL() async { diff --git a/Tests/MaliciousSiteProtectionTests/MaliciousSiteProtectionDataManagerTests.swift b/Tests/MaliciousSiteProtectionTests/MaliciousSiteProtectionDataManagerTests.swift new file mode 100644 index 000000000..a6763c2f3 --- /dev/null +++ b/Tests/MaliciousSiteProtectionTests/MaliciousSiteProtectionDataManagerTests.swift @@ -0,0 +1,213 @@ +// +// MaliciousSiteProtectionDataManagerTests.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 XCTest + +@testable import MaliciousSiteProtection + +class MaliciousSiteProtectionDataManagerTests: XCTestCase { + var embeddedDataProvider: MockMaliciousSiteProtectionEmbeddedDataProvider! + enum Constants { + static let hashPrefixesFileName = "phishingHashPrefixes.json" + static let filterSetFileName = "phishingFilterSet.json" + } + let datasetFiles: [String] = [Constants.hashPrefixesFileName, Constants.filterSetFileName, "revision.txt"] + var dataManager: MaliciousSiteProtection.DataManager! + var fileStore: MaliciousSiteProtection.FileStoring! + + override func setUp() { + super.setUp() + embeddedDataProvider = MockMaliciousSiteProtectionEmbeddedDataProvider() + fileStore = MockMaliciousSiteProtectionFileStore() + dataManager = MaliciousSiteProtection.DataManager(embeddedDataProvider: embeddedDataProvider, fileStore: fileStore) + } + + override func tearDown() { + embeddedDataProvider = nil + dataManager = nil + super.tearDown() + } + + func clearDatasets() { + for fileName in datasetFiles { + let emptyData = Data() + fileStore.write(data: emptyData, to: fileName) + } + } + + func testWhenNoDataSavedThenProviderDataReturned() async { + clearDatasets() + let expectedFilerSet = Set([Filter(hash: "some", regex: "some")]) + let expectedHashPrefix = Set(["sassa"]) + embeddedDataProvider.shouldReturnFilterSet(set: expectedFilerSet) + embeddedDataProvider.shouldReturnHashPrefixes(set: expectedHashPrefix) + + let actualFilterSet = dataManager.filterSet + let actualHashPrefix = dataManager.hashPrefixes + + XCTAssertEqual(actualFilterSet, expectedFilerSet) + XCTAssertEqual(actualHashPrefix, expectedHashPrefix) + } + + func testWhenEmbeddedRevisionNewerThanOnDisk_ThenLoadEmbedded() async { + let encoder = JSONEncoder() + // On Disk Data Setup + fileStore.write(data: "1".utf8data, to: "revision.txt") + let onDiskFilterSet = Set([Filter(hash: "other", regex: "other")]) + let filterSetData = try! encoder.encode(Array(onDiskFilterSet)) + let onDiskHashPrefix = Set(["faffa"]) + let hashPrefixData = try! encoder.encode(Array(onDiskHashPrefix)) + fileStore.write(data: filterSetData, to: Constants.filterSetFileName) + fileStore.write(data: hashPrefixData, to: Constants.hashPrefixesFileName) + + // Embedded Data Setup + embeddedDataProvider.embeddedRevision = 5 + let embeddedFilterSet = Set([Filter(hash: "some", regex: "some")]) + let embeddedHashPrefix = Set(["sassa"]) + embeddedDataProvider.shouldReturnFilterSet(set: embeddedFilterSet) + embeddedDataProvider.shouldReturnHashPrefixes(set: embeddedHashPrefix) + + let actualRevision = dataManager.currentRevision + let actualFilterSet = dataManager.filterSet + let actualHashPrefix = dataManager.hashPrefixes + + XCTAssertEqual(actualFilterSet, embeddedFilterSet) + XCTAssertEqual(actualHashPrefix, embeddedHashPrefix) + XCTAssertEqual(actualRevision, 5) + } + + func testWhenEmbeddedRevisionOlderThanOnDisk_ThenDontLoadEmbedded() async { + let encoder = JSONEncoder() + // On Disk Data Setup + fileStore.write(data: "6".utf8data, to: "revision.txt") + let onDiskFilterSet = Set([Filter(hash: "other", regex: "other")]) + let filterSetData = try! encoder.encode(Array(onDiskFilterSet)) + let onDiskHashPrefix = Set(["faffa"]) + let hashPrefixData = try! encoder.encode(Array(onDiskHashPrefix)) + fileStore.write(data: filterSetData, to: Constants.filterSetFileName) + fileStore.write(data: hashPrefixData, to: Constants.hashPrefixesFileName) + + // Embedded Data Setup + embeddedDataProvider.embeddedRevision = 1 + let embeddedFilterSet = Set([Filter(hash: "some", regex: "some")]) + let embeddedHashPrefix = Set(["sassa"]) + embeddedDataProvider.shouldReturnFilterSet(set: embeddedFilterSet) + embeddedDataProvider.shouldReturnHashPrefixes(set: embeddedHashPrefix) + + let actualRevision = dataManager.currentRevision + let actualFilterSet = dataManager.filterSet + let actualHashPrefix = dataManager.hashPrefixes + + XCTAssertEqual(actualFilterSet, onDiskFilterSet) + XCTAssertEqual(actualHashPrefix, onDiskHashPrefix) + XCTAssertEqual(actualRevision, 6) + } + + func testWriteAndLoadData() async { + // Get and write data + let expectedHashPrefixes = Set(["aabb"]) + let expectedFilterSet = Set([Filter(hash: "dummyhash", regex: "dummyregex")]) + let expectedRevision = 65 + + dataManager.saveHashPrefixes(set: expectedHashPrefixes) + dataManager.saveFilterSet(set: expectedFilterSet) + dataManager.saveRevision(expectedRevision) + + XCTAssertEqual(dataManager.filterSet, expectedFilterSet) + XCTAssertEqual(dataManager.hashPrefixes, expectedHashPrefixes) + XCTAssertEqual(dataManager.currentRevision, expectedRevision) + + // Test decode JSON data to expected types + let storedHashPrefixesData = fileStore.read(from: Constants.hashPrefixesFileName) + let storedFilterSetData = fileStore.read(from: Constants.filterSetFileName) + let storedRevisionData = fileStore.read(from: "revision.txt") + + let decoder = JSONDecoder() + if let storedHashPrefixes = try? decoder.decode(Set.self, from: storedHashPrefixesData!), + let storedFilterSet = try? decoder.decode(Set.self, from: storedFilterSetData!), + let storedRevisionString = String(data: storedRevisionData!, encoding: .utf8), + let storedRevision = Int(storedRevisionString.trimmingCharacters(in: .whitespacesAndNewlines)) { + + XCTAssertEqual(storedFilterSet, expectedFilterSet) + XCTAssertEqual(storedHashPrefixes, expectedHashPrefixes) + XCTAssertEqual(storedRevision, expectedRevision) + } else { + XCTFail("Failed to decode stored PhishingDetection data") + } + } + + func testLazyLoadingDoesNotReturnStaleData() async { + clearDatasets() + + // Set up initial data + let initialFilterSet = Set([Filter(hash: "initial", regex: "initial")]) + let initialHashPrefixes = Set(["initialPrefix"]) + embeddedDataProvider.shouldReturnFilterSet(set: initialFilterSet) + embeddedDataProvider.shouldReturnHashPrefixes(set: initialHashPrefixes) + + // Access the lazy-loaded properties to trigger loading + let loadedFilterSet = dataManager.filterSet + let loadedHashPrefixes = dataManager.hashPrefixes + + // Validate loaded data matches initial data + XCTAssertEqual(loadedFilterSet, initialFilterSet) + XCTAssertEqual(loadedHashPrefixes, initialHashPrefixes) + + // Update in-memory data + let updatedFilterSet = Set([Filter(hash: "updated", regex: "updated")]) + let updatedHashPrefixes = Set(["updatedPrefix"]) + dataManager.saveFilterSet(set: updatedFilterSet) + dataManager.saveHashPrefixes(set: updatedHashPrefixes) + + // Access lazy-loaded properties again + let reloadedFilterSet = dataManager.filterSet + let reloadedHashPrefixes = dataManager.hashPrefixes + + // Validate reloaded data matches updated data + XCTAssertEqual(reloadedFilterSet, updatedFilterSet) + XCTAssertEqual(reloadedHashPrefixes, updatedHashPrefixes) + + // Validate on-disk data is also updated + let storedFilterSetData = fileStore.read(from: Constants.filterSetFileName) + let storedHashPrefixesData = fileStore.read(from: Constants.hashPrefixesFileName) + + let decoder = JSONDecoder() + if let storedFilterSet = try? decoder.decode(Set.self, from: storedFilterSetData!), + let storedHashPrefixes = try? decoder.decode(Set.self, from: storedHashPrefixesData!) { + + XCTAssertEqual(storedFilterSet, updatedFilterSet) + XCTAssertEqual(storedHashPrefixes, updatedHashPrefixes) + } else { + XCTFail("Failed to decode stored PhishingDetection data after update") + } + } + +} + +class MockMaliciousSiteProtectionFileStore: MaliciousSiteProtection.FileStoring { + private var data: [String: Data] = [:] + + func write(data: Data, to filename: String) { + self.data[filename] = data + } + + func read(from filename: String) -> Data? { + return data[filename] + } +} diff --git a/Tests/MaliciousSiteProtectionTests/MaliciousSiteProtectionEmbeddedDataProviderTest.swift b/Tests/MaliciousSiteProtectionTests/MaliciousSiteProtectionEmbeddedDataProviderTest.swift new file mode 100644 index 000000000..6246e1f93 --- /dev/null +++ b/Tests/MaliciousSiteProtectionTests/MaliciousSiteProtectionEmbeddedDataProviderTest.swift @@ -0,0 +1,48 @@ +// +// MaliciousSiteProtectionEmbeddedDataProviderTest.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 XCTest + +@testable import MaliciousSiteProtection + +class MaliciousSiteProtectionEmbeddedDataProviderTest: XCTestCase { + var filterSetURL: URL! + var hashPrefixURL: URL! + var dataProvider: MaliciousSiteProtection.EmbeddedDataProvider! + + override func setUp() { + super.setUp() + filterSetURL = Bundle.module.url(forResource: "phishingFilterSet", withExtension: "json")! + hashPrefixURL = Bundle.module.url(forResource: "phishingHashPrefixes", withExtension: "json")! + } + + override func tearDown() { + filterSetURL = nil + hashPrefixURL = nil + dataProvider = nil + super.tearDown() + } + + func testDataProviderLoadsJSON() { + dataProvider = .init(revision: 0, filterSetURL: filterSetURL, filterSetDataSHA: "4fd2868a4f264501ec175ab866504a2a96c8d21a3b5195b405a4a83b51eae504", hashPrefixURL: hashPrefixURL, hashPrefixDataSHA: "21b047a9950fcaf86034a6b16181e18815cb8d276386d85c8977ca8c5f8aa05f") + let expectedFilter = Filter(hash: "e4753ddad954dafd4ff4ef67f82b3c1a2db6ef4a51bda43513260170e558bd13", regex: "(?i)^https?\\:\\/\\/privacy-test-pages\\.site(?:\\:(?:80|443))?\\/security\\/badware\\/phishing\\.html$") + XCTAssertTrue(dataProvider.loadEmbeddedFilterSet().contains(expectedFilter)) + XCTAssertTrue(dataProvider.loadEmbeddedHashPrefixes().contains("012db806")) + } + +} diff --git a/Tests/PhishingDetectionTests/PhishingDetectionURLTests.swift b/Tests/MaliciousSiteProtectionTests/MaliciousSiteProtectionURLTests.swift similarity index 92% rename from Tests/PhishingDetectionTests/PhishingDetectionURLTests.swift rename to Tests/MaliciousSiteProtectionTests/MaliciousSiteProtectionURLTests.swift index ea0576369..8df462b3e 100644 --- a/Tests/PhishingDetectionTests/PhishingDetectionURLTests.swift +++ b/Tests/MaliciousSiteProtectionTests/MaliciousSiteProtectionURLTests.swift @@ -1,5 +1,5 @@ // -// PhishingDetectionURLTests.swift +// MaliciousSiteProtectionURLTests.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -18,9 +18,10 @@ import Foundation import XCTest -@testable import PhishingDetection -class PhishingDetectionURLTests: XCTestCase { +@testable import MaliciousSiteProtection + +class MaliciousSiteProtectionURLTests: XCTestCase { let testURLs = [ "http://www.example.com/security/badware/phishing.html#frags", diff --git a/Tests/MaliciousSiteProtectionTests/MaliciousSiteProtectionUpdateManagerTests.swift b/Tests/MaliciousSiteProtectionTests/MaliciousSiteProtectionUpdateManagerTests.swift new file mode 100644 index 000000000..53d68dbd5 --- /dev/null +++ b/Tests/MaliciousSiteProtectionTests/MaliciousSiteProtectionUpdateManagerTests.swift @@ -0,0 +1,143 @@ +// +// MaliciousSiteProtectionUpdateManagerTests.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 XCTest + +@testable import MaliciousSiteProtection + +class MaliciousSiteProtectionUpdateManagerTests: XCTestCase { + var updateManager: MaliciousSiteProtection.UpdateManager! + var dataManager: MaliciousSiteProtection.DataManaging! + var apiClient: MaliciousSiteProtection.APIClientProtocol! + + override func setUp() async throws { + try await super.setUp() + apiClient = MockMaliciousSiteProtectionAPIClient() + dataManager = MockMaliciousSiteProtectionDataManager() + updateManager = MaliciousSiteProtection.UpdateManager(apiClient: apiClient, dataManager: dataManager) + dataManager.saveRevision(0) + await updateManager.updateFilterSet() + await updateManager.updateHashPrefixes() + } + + override func tearDown() { + updateManager = nil + dataManager = nil + apiClient = nil + super.tearDown() + } + + func testUpdateHashPrefixes() async { + await updateManager.updateHashPrefixes() + XCTAssertFalse(dataManager.hashPrefixes.isEmpty, "Hash prefixes should not be empty after update.") + XCTAssertEqual(dataManager.hashPrefixes, [ + "aa00bb11", + "bb00cc11", + "cc00dd11", + "dd00ee11", + "a379a6f6" + ]) + } + + func testUpdateFilterSet() async { + await updateManager.updateFilterSet() + XCTAssertEqual(dataManager.filterSet, [ + Filter(hash: "testhash1", regex: ".*example.*"), + Filter(hash: "testhash2", regex: ".*test.*") + ]) + } + + func testRevision1AddsAndDeletesData() async { + let expectedFilterSet: Set = [ + Filter(hash: "testhash2", regex: ".*test.*"), + Filter(hash: "testhash3", regex: ".*test.*") + ] + let expectedHashPrefixes: Set = [ + "aa00bb11", + "bb00cc11", + "a379a6f6", + "93e2435e" + ] + + // Save revision and update the filter set and hash prefixes + dataManager.saveRevision(1) + await updateManager.updateFilterSet() + await updateManager.updateHashPrefixes() + + XCTAssertEqual(dataManager.filterSet, expectedFilterSet, "Filter set should match the expected set after update.") + XCTAssertEqual(dataManager.hashPrefixes, expectedHashPrefixes, "Hash prefixes should match the expected set after update.") + } + + func testRevision2AddsAndDeletesData() async { + let expectedFilterSet: Set = [ + Filter(hash: "testhash4", regex: ".*test.*"), + Filter(hash: "testhash1", regex: ".*example.*") + ] + let expectedHashPrefixes: Set = [ + "aa00bb11", + "a379a6f6", + "c0be0d0a6", + "dd00ee11", + "cc00dd11" + ] + + // Save revision and update the filter set and hash prefixes + dataManager.saveRevision(2) + await updateManager.updateFilterSet() + await updateManager.updateHashPrefixes() + + XCTAssertEqual(dataManager.filterSet, expectedFilterSet, "Filter set should match the expected set after update.") + XCTAssertEqual(dataManager.hashPrefixes, expectedHashPrefixes, "Hash prefixes should match the expected set after update.") + } + + func testRevision3AddsAndDeletesNothing() async { + let expectedFilterSet = dataManager.filterSet + let expectedHashPrefixes = dataManager.hashPrefixes + + // Save revision and update the filter set and hash prefixes + dataManager.saveRevision(3) + await updateManager.updateFilterSet() + await updateManager.updateHashPrefixes() + + XCTAssertEqual(dataManager.filterSet, expectedFilterSet, "Filter set should match the expected set after update.") + XCTAssertEqual(dataManager.hashPrefixes, expectedHashPrefixes, "Hash prefixes should match the expected set after update.") + } + + func testRevision4AddsAndDeletesData() async { + let expectedFilterSet: Set = [ + Filter(hash: "testhash2", regex: ".*test.*"), + Filter(hash: "testhash1", regex: ".*example.*"), + Filter(hash: "testhash5", regex: ".*test.*") + ] + let expectedHashPrefixes: Set = [ + "a379a6f6", + "dd00ee11", + "cc00dd11", + "bb00cc11" + ] + + // Save revision and update the filter set and hash prefixes + dataManager.saveRevision(4) + await updateManager.updateFilterSet() + await updateManager.updateHashPrefixes() + + XCTAssertEqual(dataManager.filterSet, expectedFilterSet, "Filter set should match the expected set after update.") + XCTAssertEqual(dataManager.hashPrefixes, expectedHashPrefixes, "Hash prefixes should match the expected set after update.") + } +} diff --git a/Tests/PhishingDetectionTests/Mocks/BackgroundActivitySchedulerMock.swift b/Tests/MaliciousSiteProtectionTests/Mocks/BackgroundActivitySchedulerMock.swift similarity index 94% rename from Tests/PhishingDetectionTests/Mocks/BackgroundActivitySchedulerMock.swift rename to Tests/MaliciousSiteProtectionTests/Mocks/BackgroundActivitySchedulerMock.swift index 86b79d477..4058cbc70 100644 --- a/Tests/PhishingDetectionTests/Mocks/BackgroundActivitySchedulerMock.swift +++ b/Tests/MaliciousSiteProtectionTests/Mocks/BackgroundActivitySchedulerMock.swift @@ -17,8 +17,8 @@ // import Foundation -import PhishingDetection - +import MaliciousSiteProtection +// TODO: to be dropped actor MockBackgroundActivityScheduler: BackgroundActivityScheduling { var startCalled = false var stopCalled = false diff --git a/Tests/PhishingDetectionTests/Mocks/EventMappingMock.swift b/Tests/MaliciousSiteProtectionTests/Mocks/MockEventMapping.swift similarity index 80% rename from Tests/PhishingDetectionTests/Mocks/EventMappingMock.swift rename to Tests/MaliciousSiteProtectionTests/Mocks/MockEventMapping.swift index 7c736c7e3..1edbb98a2 100644 --- a/Tests/PhishingDetectionTests/Mocks/EventMappingMock.swift +++ b/Tests/MaliciousSiteProtectionTests/Mocks/MockEventMapping.swift @@ -1,5 +1,5 @@ // -// EventMappingMock.swift +// MockEventMapping.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -15,13 +15,14 @@ // See the License for the specific language governing permissions and // limitations under the License. // -import Foundation + import Common -import PhishingDetection +import Foundation +import MaliciousSiteProtection import PixelKit -public class MockEventMapping: EventMapping { - static var events: [PhishingDetectionEvents] = [] +public class MockEventMapping: EventMapping { + static var events: [MaliciousSiteProtection.Event] = [] static var clientSideHitParam: String? static var errorParam: Error? @@ -39,7 +40,7 @@ public class MockEventMapping: EventMapping { } } - override init(mapping: @escaping EventMapping.Mapping) { + override init(mapping: @escaping EventMapping.Mapping) { fatalError("Use init()") } } diff --git a/Tests/PhishingDetectionTests/Mocks/PhishingDetectorMock.swift b/Tests/MaliciousSiteProtectionTests/Mocks/MockMaliciousSiteDetector.swift similarity index 69% rename from Tests/PhishingDetectionTests/Mocks/PhishingDetectorMock.swift rename to Tests/MaliciousSiteProtectionTests/Mocks/MockMaliciousSiteDetector.swift index 4a56474e0..0d54cd459 100644 --- a/Tests/PhishingDetectionTests/Mocks/PhishingDetectorMock.swift +++ b/Tests/MaliciousSiteProtectionTests/Mocks/MockMaliciousSiteDetector.swift @@ -1,5 +1,5 @@ // -// PhishingDetectorMock.swift +// MockMaliciousSiteDetector.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -17,14 +17,14 @@ // import Foundation -import PhishingDetection +import MaliciousSiteProtection -public class MockPhishingDetector: PhishingDetecting { - private var mockClient: PhishingDetectionClientProtocol +public class MockMaliciousSiteDetector: MaliciousSiteDetecting { + private var mockClient: MaliciousSiteProtection.APIClientProtocol public var didCallIsMalicious: Bool = false init() { - self.mockClient = MockPhishingDetectionClient() + self.mockClient = MockMaliciousSiteProtectionAPIClient() } public func getMatches(hashPrefix: String) async -> Set { @@ -32,7 +32,7 @@ public class MockPhishingDetector: PhishingDetecting { return Set(matches) } - public func isMalicious(url: URL) async -> Bool { - return url.absoluteString.contains("malicious") + public func evaluate(_ url: URL) async -> ThreatKind? { + return url.absoluteString.contains("malicious") ? .phishing : nil } } diff --git a/Tests/PhishingDetectionTests/Mocks/PhishingDetectionClientMock.swift b/Tests/MaliciousSiteProtectionTests/Mocks/MockMaliciousSiteProtectionAPIClient.swift similarity index 52% rename from Tests/PhishingDetectionTests/Mocks/PhishingDetectionClientMock.swift rename to Tests/MaliciousSiteProtectionTests/Mocks/MockMaliciousSiteProtectionAPIClient.swift index 9f39598b2..ad2a31fe9 100644 --- a/Tests/PhishingDetectionTests/Mocks/PhishingDetectionClientMock.swift +++ b/Tests/MaliciousSiteProtectionTests/Mocks/MockMaliciousSiteProtectionAPIClient.swift @@ -1,5 +1,5 @@ // -// PhishingDetectionClientMock.swift +// MockMaliciousSiteProtectionAPIClient.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -17,68 +17,68 @@ // import Foundation -import PhishingDetection +import MaliciousSiteProtection -public class MockPhishingDetectionClient: PhishingDetectionClientProtocol { +public class MockMaliciousSiteProtectionAPIClient: MaliciousSiteProtection.APIClientProtocol { public var updateHashPrefixesWasCalled: Bool = false public var updateFilterSetsWasCalled: Bool = false - private var filterRevisions: [Int: FilterSetResponse] = [ - 0: FilterSetResponse(insert: [ - Filter(hashValue: "testhash1", regex: ".*example.*"), - Filter(hashValue: "testhash2", regex: ".*test.*") + private var filterRevisions: [Int: APIClient.FiltersChangeSetResponse] = [ + 0: .init(insert: [ + Filter(hash: "testhash1", regex: ".*example.*"), + Filter(hash: "testhash2", regex: ".*test.*") ], delete: [], revision: 0, replace: true), - 1: FilterSetResponse(insert: [ - Filter(hashValue: "testhash3", regex: ".*test.*") + 1: .init(insert: [ + Filter(hash: "testhash3", regex: ".*test.*") ], delete: [ - Filter(hashValue: "testhash1", regex: ".*example.*"), + Filter(hash: "testhash1", regex: ".*example.*"), ], revision: 1, replace: false), - 2: FilterSetResponse(insert: [ - Filter(hashValue: "testhash4", regex: ".*test.*") + 2: .init(insert: [ + Filter(hash: "testhash4", regex: ".*test.*") ], delete: [ - Filter(hashValue: "testhash2", regex: ".*test.*"), + Filter(hash: "testhash2", regex: ".*test.*"), ], revision: 2, replace: false), - 4: FilterSetResponse(insert: [ - Filter(hashValue: "testhash5", regex: ".*test.*") + 4: .init(insert: [ + Filter(hash: "testhash5", regex: ".*test.*") ], delete: [ - Filter(hashValue: "testhash3", regex: ".*test.*"), + Filter(hash: "testhash3", regex: ".*test.*"), ], revision: 4, replace: false) ] - private var hashPrefixRevisions: [Int: HashPrefixResponse] = [ - 0: HashPrefixResponse(insert: [ + private var hashPrefixRevisions: [Int: APIClient.HashPrefixesChangeSetResponse] = [ + 0: .init(insert: [ "aa00bb11", "bb00cc11", "cc00dd11", "dd00ee11", "a379a6f6" ], delete: [], revision: 0, replace: true), - 1: HashPrefixResponse(insert: ["93e2435e"], delete: [ + 1: .init(insert: ["93e2435e"], delete: [ "cc00dd11", "dd00ee11", ], revision: 1, replace: false), - 2: HashPrefixResponse(insert: ["c0be0d0a6"], delete: [ + 2: .init(insert: ["c0be0d0a6"], delete: [ "bb00cc11", ], revision: 2, replace: false), - 4: HashPrefixResponse(insert: ["a379a6f6"], delete: [ + 4: .init(insert: ["a379a6f6"], delete: [ "aa00bb11", ], revision: 4, replace: false) ] - public func getFilterSet(revision: Int) async -> FilterSetResponse { + public func getFilterSet(revision: Int) async -> APIClient.FiltersChangeSetResponse { updateFilterSetsWasCalled = true - return filterRevisions[revision] ?? FilterSetResponse(insert: [], delete: [], revision: revision, replace: false) + return filterRevisions[revision] ?? .init(insert: [], delete: [], revision: revision, replace: false) } - public func getHashPrefixes(revision: Int) async -> HashPrefixResponse { + public func getHashPrefixes(revision: Int) async -> APIClient.HashPrefixesChangeSetResponse { updateHashPrefixesWasCalled = true - return hashPrefixRevisions[revision] ?? HashPrefixResponse(insert: [], delete: [], revision: revision, replace: false) + return hashPrefixRevisions[revision] ?? .init(insert: [], delete: [], revision: revision, replace: false) } public func getMatches(hashPrefix: String) async -> [Match] { return [ - Match(hostname: "example.com", url: "https://example.com/mal", regex: ".", hash: "a379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce1947"), - Match(hostname: "test.com", url: "https://test.com/mal", regex: ".*test.*", hash: "aa00bb11aa00cc11bb00cc11") + Match(hostname: "example.com", url: "https://example.com/mal", regex: ".", hash: "a379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce1947", category: nil), + Match(hostname: "test.com", url: "https://test.com/mal", regex: ".*test.*", hash: "aa00bb11aa00cc11bb00cc11", category: nil) ] } } diff --git a/Tests/PhishingDetectionTests/Mocks/PhishingDetectionDataStoreMock.swift b/Tests/MaliciousSiteProtectionTests/Mocks/MockMaliciousSiteProtectionDataManager.swift similarity index 81% rename from Tests/PhishingDetectionTests/Mocks/PhishingDetectionDataStoreMock.swift rename to Tests/MaliciousSiteProtectionTests/Mocks/MockMaliciousSiteProtectionDataManager.swift index 54521419c..64d82bbea 100644 --- a/Tests/PhishingDetectionTests/Mocks/PhishingDetectionDataStoreMock.swift +++ b/Tests/MaliciousSiteProtectionTests/Mocks/MockMaliciousSiteProtectionDataManager.swift @@ -1,5 +1,5 @@ // -// PhishingDetectionDataStoreMock.swift +// MockMaliciousSiteProtectionDataManager.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -17,9 +17,9 @@ // import Foundation -import PhishingDetection +import MaliciousSiteProtection -public class MockPhishingDetectionDataStore: PhishingDetectionDataSaving { +public class MockMaliciousSiteProtectionDataManager: MaliciousSiteProtection.DataManaging { public var filterSet: Set public var hashPrefixes: Set public var currentRevision: Int @@ -30,7 +30,7 @@ public class MockPhishingDetectionDataStore: PhishingDetectionDataSaving { currentRevision = 0 } - public func saveFilterSet(set: Set) { + public func saveFilterSet(set: Set) { filterSet = set } diff --git a/Tests/PhishingDetectionTests/Mocks/PhishingDetectionDataProviderMock.swift b/Tests/MaliciousSiteProtectionTests/Mocks/MockMaliciousSiteProtectionEmbeddedDataProvider.swift similarity index 82% rename from Tests/PhishingDetectionTests/Mocks/PhishingDetectionDataProviderMock.swift rename to Tests/MaliciousSiteProtectionTests/Mocks/MockMaliciousSiteProtectionEmbeddedDataProvider.swift index 79d4d5d6b..9bb44bbe2 100644 --- a/Tests/PhishingDetectionTests/Mocks/PhishingDetectionDataProviderMock.swift +++ b/Tests/MaliciousSiteProtectionTests/Mocks/MockMaliciousSiteProtectionEmbeddedDataProvider.swift @@ -1,5 +1,5 @@ // -// PhishingDetectionDataProviderMock.swift +// MockMaliciousSiteProtectionEmbeddedDataProvider.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -17,14 +17,14 @@ // import Foundation -import PhishingDetection +import MaliciousSiteProtection -public class MockPhishingDetectionDataProvider: PhishingDetectionDataProviding { +public class MockMaliciousSiteProtectionEmbeddedDataProvider: MaliciousSiteProtection.EmbeddedDataProviding { public var embeddedRevision: Int = 65 var loadHashPrefixesCalled: Bool = false var loadFilterSetCalled: Bool = true var hashPrefixes: Set = ["aabb"] - var filterSet: Set = [Filter(hashValue: "dummyhash", regex: "dummyregex")] + var filterSet: Set = [Filter(hash: "dummyhash", regex: "dummyregex")] public func shouldReturnFilterSet(set: Set) { self.filterSet = set diff --git a/Tests/PhishingDetectionTests/Mocks/PhishingDetectionUpdateManagerMock.swift b/Tests/MaliciousSiteProtectionTests/Mocks/MockPhishingDetectionUpdateManager.swift similarity index 87% rename from Tests/PhishingDetectionTests/Mocks/PhishingDetectionUpdateManagerMock.swift rename to Tests/MaliciousSiteProtectionTests/Mocks/MockPhishingDetectionUpdateManager.swift index d5ca12559..3eb67c06b 100644 --- a/Tests/PhishingDetectionTests/Mocks/PhishingDetectionUpdateManagerMock.swift +++ b/Tests/MaliciousSiteProtectionTests/Mocks/MockPhishingDetectionUpdateManager.swift @@ -1,5 +1,5 @@ // -// PhishingDetectionUpdateManagerMock.swift +// MockPhishingDetectionUpdateManager.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -17,9 +17,9 @@ // import Foundation -import PhishingDetection +import MaliciousSiteProtection -public class MockPhishingDetectionUpdateManager: PhishingDetectionUpdateManaging { +public class MockPhishingDetectionUpdateManager: MaliciousSiteProtection.UpdateManaging { var didUpdateFilterSet = false var didUpdateHashPrefixes = false var completionHandler: (() -> Void)? diff --git a/Tests/PhishingDetectionTests/PhishingDetectionDataActivitiesTests.swift b/Tests/MaliciousSiteProtectionTests/PhishingDetectionDataActivitiesTests.swift similarity index 90% rename from Tests/PhishingDetectionTests/PhishingDetectionDataActivitiesTests.swift rename to Tests/MaliciousSiteProtectionTests/PhishingDetectionDataActivitiesTests.swift index 583f94789..9cb38a76f 100644 --- a/Tests/PhishingDetectionTests/PhishingDetectionDataActivitiesTests.swift +++ b/Tests/MaliciousSiteProtectionTests/PhishingDetectionDataActivitiesTests.swift @@ -18,8 +18,8 @@ import Foundation import XCTest -@testable import PhishingDetection - +@testable import MaliciousSiteProtection +// TODO: to be dropped class PhishingDetectionDataActivitiesTests: XCTestCase { var mockUpdateManager: MockPhishingDetectionUpdateManager! var activities: PhishingDetectionDataActivities! @@ -27,7 +27,7 @@ class PhishingDetectionDataActivitiesTests: XCTestCase { override func setUp() { super.setUp() mockUpdateManager = MockPhishingDetectionUpdateManager() - activities = PhishingDetectionDataActivities(hashPrefixInterval: 1, filterSetInterval: 1, phishingDetectionDataProvider: MockPhishingDetectionDataProvider(), updateManager: mockUpdateManager) + activities = PhishingDetectionDataActivities(hashPrefixInterval: 1, filterSetInterval: 1, updateManager: mockUpdateManager) } func testUpdateHashPrefixesAndFilterSetRuns() async { diff --git a/Tests/PhishingDetectionTests/Resources/filterSet.json b/Tests/MaliciousSiteProtectionTests/Resources/phishingFilterSet.json similarity index 100% rename from Tests/PhishingDetectionTests/Resources/filterSet.json rename to Tests/MaliciousSiteProtectionTests/Resources/phishingFilterSet.json diff --git a/Tests/PhishingDetectionTests/Resources/hashPrefixes.json b/Tests/MaliciousSiteProtectionTests/Resources/phishingHashPrefixes.json similarity index 100% rename from Tests/PhishingDetectionTests/Resources/hashPrefixes.json rename to Tests/MaliciousSiteProtectionTests/Resources/phishingHashPrefixes.json diff --git a/Tests/NavigationTests/Helpers/NavigationResponderMock.swift b/Tests/NavigationTests/Helpers/NavigationResponderMock.swift index d39a6ee44..fda1b2805 100644 --- a/Tests/NavigationTests/Helpers/NavigationResponderMock.swift +++ b/Tests/NavigationTests/Helpers/NavigationResponderMock.swift @@ -374,7 +374,6 @@ class NavigationResponderMock: NavigationResponder { var onDidTerminate: (@MainActor (WKProcessTerminationReason?) -> Void)? func webContentProcessDidTerminate(with reason: WKProcessTerminationReason?) { - let event = append(.didTerminate(reason)) onDidTerminate?(reason) } diff --git a/Tests/OnboardingTests/OnboardingSuggestionsViewModelsTests.swift b/Tests/OnboardingTests/OnboardingSuggestionsViewModelsTests.swift index ed7927f4f..942543505 100644 --- a/Tests/OnboardingTests/OnboardingSuggestionsViewModelsTests.swift +++ b/Tests/OnboardingTests/OnboardingSuggestionsViewModelsTests.swift @@ -147,11 +147,11 @@ class CapturingOnboardingNavigationDelegate: OnboardingNavigationDelegate { var suggestedSearchQuery: String? var urlToNavigateTo: URL? - func searchFor(_ query: String) { + func searchFromOnboarding(for query: String) { suggestedSearchQuery = query } - func navigateTo(url: URL) { + func navigateFromOnboarding(to url: URL) { urlToNavigateTo = url } } diff --git a/Tests/PhishingDetectionTests/PhishingDetectionDataProviderTest.swift b/Tests/PhishingDetectionTests/PhishingDetectionDataProviderTest.swift deleted file mode 100644 index 547f2dce8..000000000 --- a/Tests/PhishingDetectionTests/PhishingDetectionDataProviderTest.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// PhishingDetectionDataProviderTest.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 XCTest -@testable import PhishingDetection - -class PhishingDetectionDataProviderTest: XCTestCase { - var filterSetURL: URL! - var hashPrefixURL: URL! - var dataProvider: PhishingDetectionDataProvider! - - override func setUp() { - super.setUp() - filterSetURL = Bundle.module.url(forResource: "filterSet", withExtension: "json")! - hashPrefixURL = Bundle.module.url(forResource: "hashPrefixes", withExtension: "json")! - } - - override func tearDown() { - filterSetURL = nil - hashPrefixURL = nil - dataProvider = nil - super.tearDown() - } - - func testDataProviderLoadsJSON() { - dataProvider = PhishingDetectionDataProvider(revision: 0, filterSetURL: filterSetURL, filterSetDataSHA: "4fd2868a4f264501ec175ab866504a2a96c8d21a3b5195b405a4a83b51eae504", hashPrefixURL: hashPrefixURL, hashPrefixDataSHA: "21b047a9950fcaf86034a6b16181e18815cb8d276386d85c8977ca8c5f8aa05f") - let expectedFilter = Filter(hashValue: "e4753ddad954dafd4ff4ef67f82b3c1a2db6ef4a51bda43513260170e558bd13", regex: "(?i)^https?\\:\\/\\/privacy-test-pages\\.site(?:\\:(?:80|443))?\\/security\\/badware\\/phishing\\.html$") - XCTAssertTrue(dataProvider.loadEmbeddedFilterSet().contains(expectedFilter)) - XCTAssertTrue(dataProvider.loadEmbeddedHashPrefixes().contains("012db806")) - } - - func testReturnsNoneWhenSHAMismatch() { - dataProvider = PhishingDetectionDataProvider(revision: 0, filterSetURL: filterSetURL, filterSetDataSHA: "xx0", hashPrefixURL: hashPrefixURL, hashPrefixDataSHA: "00x") - XCTAssertTrue(dataProvider.loadEmbeddedFilterSet().isEmpty) - XCTAssertTrue(dataProvider.loadEmbeddedHashPrefixes().isEmpty) - } -} diff --git a/Tests/PhishingDetectionTests/PhishingDetectionDataStoreTests.swift b/Tests/PhishingDetectionTests/PhishingDetectionDataStoreTests.swift deleted file mode 100644 index 79e9fb500..000000000 --- a/Tests/PhishingDetectionTests/PhishingDetectionDataStoreTests.swift +++ /dev/null @@ -1,197 +0,0 @@ -// -// PhishingDetectionDataStoreTests.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 XCTest -@testable import PhishingDetection - -class PhishingDetectionDataStoreTests: XCTestCase { - var mockDataProvider: MockPhishingDetectionDataProvider! - let datasetFiles: [String] = ["hashPrefixes.json", "filterSet.json", "revision.txt"] - var dataStore: PhishingDetectionDataStore! - var fileStorageManager: FileStorageManager! - - override func setUp() { - super.setUp() - mockDataProvider = MockPhishingDetectionDataProvider() - fileStorageManager = MockPhishingFileStorageManager() - dataStore = PhishingDetectionDataStore(dataProvider: mockDataProvider, fileStorageManager: fileStorageManager) - } - - override func tearDown() { - mockDataProvider = nil - dataStore = nil - super.tearDown() - } - - func clearDatasets() { - for fileName in datasetFiles { - let emptyData = Data() - fileStorageManager.write(data: emptyData, to: fileName) - } - } - - func testWhenNoDataSavedThenProviderDataReturned() async { - clearDatasets() - let expectedFilerSet = Set([Filter(hashValue: "some", regex: "some")]) - let expectedHashPrefix = Set(["sassa"]) - mockDataProvider.shouldReturnFilterSet(set: expectedFilerSet) - mockDataProvider.shouldReturnHashPrefixes(set: expectedHashPrefix) - - let actualFilterSet = dataStore.filterSet - let actualHashPrefix = dataStore.hashPrefixes - - XCTAssertEqual(actualFilterSet, expectedFilerSet) - XCTAssertEqual(actualHashPrefix, expectedHashPrefix) - } - - func testWhenEmbeddedRevisionNewerThanOnDisk_ThenLoadEmbedded() async { - let encoder = JSONEncoder() - // On Disk Data Setup - fileStorageManager.write(data: "1".utf8data, to: "revision.txt") - let onDiskFilterSet = Set([Filter(hashValue: "other", regex: "other")]) - let filterSetData = try! encoder.encode(Array(onDiskFilterSet)) - let onDiskHashPrefix = Set(["faffa"]) - let hashPrefixData = try! encoder.encode(Array(onDiskHashPrefix)) - fileStorageManager.write(data: filterSetData, to: "filterSet.json") - fileStorageManager.write(data: hashPrefixData, to: "hashPrefixes.json") - - // Embedded Data Setup - mockDataProvider.embeddedRevision = 5 - let embeddedFilterSet = Set([Filter(hashValue: "some", regex: "some")]) - let embeddedHashPrefix = Set(["sassa"]) - mockDataProvider.shouldReturnFilterSet(set: embeddedFilterSet) - mockDataProvider.shouldReturnHashPrefixes(set: embeddedHashPrefix) - - let actualRevision = dataStore.currentRevision - let actualFilterSet = dataStore.filterSet - let actualHashPrefix = dataStore.hashPrefixes - - XCTAssertEqual(actualFilterSet, embeddedFilterSet) - XCTAssertEqual(actualHashPrefix, embeddedHashPrefix) - XCTAssertEqual(actualRevision, 5) - } - - func testWhenEmbeddedRevisionOlderThanOnDisk_ThenDontLoadEmbedded() async { - let encoder = JSONEncoder() - // On Disk Data Setup - fileStorageManager.write(data: "6".utf8data, to: "revision.txt") - let onDiskFilterSet = Set([Filter(hashValue: "other", regex: "other")]) - let filterSetData = try! encoder.encode(Array(onDiskFilterSet)) - let onDiskHashPrefix = Set(["faffa"]) - let hashPrefixData = try! encoder.encode(Array(onDiskHashPrefix)) - fileStorageManager.write(data: filterSetData, to: "filterSet.json") - fileStorageManager.write(data: hashPrefixData, to: "hashPrefixes.json") - - // Embedded Data Setup - mockDataProvider.embeddedRevision = 1 - let embeddedFilterSet = Set([Filter(hashValue: "some", regex: "some")]) - let embeddedHashPrefix = Set(["sassa"]) - mockDataProvider.shouldReturnFilterSet(set: embeddedFilterSet) - mockDataProvider.shouldReturnHashPrefixes(set: embeddedHashPrefix) - - let actualRevision = dataStore.currentRevision - let actualFilterSet = dataStore.filterSet - let actualHashPrefix = dataStore.hashPrefixes - - XCTAssertEqual(actualFilterSet, onDiskFilterSet) - XCTAssertEqual(actualHashPrefix, onDiskHashPrefix) - XCTAssertEqual(actualRevision, 6) - } - - func testWriteAndLoadData() async { - // Get and write data - let expectedHashPrefixes = Set(["aabb"]) - let expectedFilterSet = Set([Filter(hashValue: "dummyhash", regex: "dummyregex")]) - let expectedRevision = 65 - - dataStore.saveHashPrefixes(set: expectedHashPrefixes) - dataStore.saveFilterSet(set: expectedFilterSet) - dataStore.saveRevision(expectedRevision) - - XCTAssertEqual(dataStore.filterSet, expectedFilterSet) - XCTAssertEqual(dataStore.hashPrefixes, expectedHashPrefixes) - XCTAssertEqual(dataStore.currentRevision, expectedRevision) - - // Test decode JSON data to expected types - let storedHashPrefixesData = fileStorageManager.read(from: "hashPrefixes.json") - let storedFilterSetData = fileStorageManager.read(from: "filterSet.json") - let storedRevisionData = fileStorageManager.read(from: "revision.txt") - - let decoder = JSONDecoder() - if let storedHashPrefixes = try? decoder.decode(Set.self, from: storedHashPrefixesData!), - let storedFilterSet = try? decoder.decode(Set.self, from: storedFilterSetData!), - let storedRevisionString = String(data: storedRevisionData!, encoding: .utf8), - let storedRevision = Int(storedRevisionString.trimmingCharacters(in: .whitespacesAndNewlines)) { - - XCTAssertEqual(storedFilterSet, expectedFilterSet) - XCTAssertEqual(storedHashPrefixes, expectedHashPrefixes) - XCTAssertEqual(storedRevision, expectedRevision) - } else { - XCTFail("Failed to decode stored PhishingDetection data") - } - } - - func testLazyLoadingDoesNotReturnStaleData() async { - clearDatasets() - - // Set up initial data - let initialFilterSet = Set([Filter(hashValue: "initial", regex: "initial")]) - let initialHashPrefixes = Set(["initialPrefix"]) - mockDataProvider.shouldReturnFilterSet(set: initialFilterSet) - mockDataProvider.shouldReturnHashPrefixes(set: initialHashPrefixes) - - // Access the lazy-loaded properties to trigger loading - let loadedFilterSet = dataStore.filterSet - let loadedHashPrefixes = dataStore.hashPrefixes - - // Validate loaded data matches initial data - XCTAssertEqual(loadedFilterSet, initialFilterSet) - XCTAssertEqual(loadedHashPrefixes, initialHashPrefixes) - - // Update in-memory data - let updatedFilterSet = Set([Filter(hashValue: "updated", regex: "updated")]) - let updatedHashPrefixes = Set(["updatedPrefix"]) - dataStore.saveFilterSet(set: updatedFilterSet) - dataStore.saveHashPrefixes(set: updatedHashPrefixes) - - // Access lazy-loaded properties again - let reloadedFilterSet = dataStore.filterSet - let reloadedHashPrefixes = dataStore.hashPrefixes - - // Validate reloaded data matches updated data - XCTAssertEqual(reloadedFilterSet, updatedFilterSet) - XCTAssertEqual(reloadedHashPrefixes, updatedHashPrefixes) - - // Validate on-disk data is also updated - let storedFilterSetData = fileStorageManager.read(from: "filterSet.json") - let storedHashPrefixesData = fileStorageManager.read(from: "hashPrefixes.json") - - let decoder = JSONDecoder() - if let storedFilterSet = try? decoder.decode(Set.self, from: storedFilterSetData!), - let storedHashPrefixes = try? decoder.decode(Set.self, from: storedHashPrefixesData!) { - - XCTAssertEqual(storedFilterSet, updatedFilterSet) - XCTAssertEqual(storedHashPrefixes, updatedHashPrefixes) - } else { - XCTFail("Failed to decode stored PhishingDetection data after update") - } - } - -} diff --git a/Tests/PhishingDetectionTests/PhishingDetectionUpdateManagerTests.swift b/Tests/PhishingDetectionTests/PhishingDetectionUpdateManagerTests.swift deleted file mode 100644 index 6fec6c134..000000000 --- a/Tests/PhishingDetectionTests/PhishingDetectionUpdateManagerTests.swift +++ /dev/null @@ -1,155 +0,0 @@ -// -// PhishingDetectionUpdateManagerTests.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 XCTest -@testable import PhishingDetection - -class PhishingDetectionUpdateManagerTests: XCTestCase { - var updateManager: PhishingDetectionUpdateManager! - var dataStore: PhishingDetectionDataSaving! - var mockClient: MockPhishingDetectionClient! - - override func setUp() async throws { - try await super.setUp() - mockClient = MockPhishingDetectionClient() - dataStore = MockPhishingDetectionDataStore() - updateManager = PhishingDetectionUpdateManager(client: mockClient, dataStore: dataStore) - dataStore.saveRevision(0) - await updateManager.updateFilterSet() - await updateManager.updateHashPrefixes() - } - - override func tearDown() { - updateManager = nil - dataStore = nil - mockClient = nil - super.tearDown() - } - - func testUpdateHashPrefixes() async { - await updateManager.updateHashPrefixes() - XCTAssertFalse(dataStore.hashPrefixes.isEmpty, "Hash prefixes should not be empty after update.") - XCTAssertEqual(dataStore.hashPrefixes, [ - "aa00bb11", - "bb00cc11", - "cc00dd11", - "dd00ee11", - "a379a6f6" - ]) - } - - func testUpdateFilterSet() async { - await updateManager.updateFilterSet() - XCTAssertEqual(dataStore.filterSet, [ - Filter(hashValue: "testhash1", regex: ".*example.*"), - Filter(hashValue: "testhash2", regex: ".*test.*") - ]) - } - - func testRevision1AddsAndDeletesData() async { - let expectedFilterSet: Set = [ - Filter(hashValue: "testhash2", regex: ".*test.*"), - Filter(hashValue: "testhash3", regex: ".*test.*") - ] - let expectedHashPrefixes: Set = [ - "aa00bb11", - "bb00cc11", - "a379a6f6", - "93e2435e" - ] - - // Save revision and update the filter set and hash prefixes - dataStore.saveRevision(1) - await updateManager.updateFilterSet() - await updateManager.updateHashPrefixes() - - XCTAssertEqual(dataStore.filterSet, expectedFilterSet, "Filter set should match the expected set after update.") - XCTAssertEqual(dataStore.hashPrefixes, expectedHashPrefixes, "Hash prefixes should match the expected set after update.") - } - - func testRevision2AddsAndDeletesData() async { - let expectedFilterSet: Set = [ - Filter(hashValue: "testhash4", regex: ".*test.*"), - Filter(hashValue: "testhash1", regex: ".*example.*") - ] - let expectedHashPrefixes: Set = [ - "aa00bb11", - "a379a6f6", - "c0be0d0a6", - "dd00ee11", - "cc00dd11" - ] - - // Save revision and update the filter set and hash prefixes - dataStore.saveRevision(2) - await updateManager.updateFilterSet() - await updateManager.updateHashPrefixes() - - XCTAssertEqual(dataStore.filterSet, expectedFilterSet, "Filter set should match the expected set after update.") - XCTAssertEqual(dataStore.hashPrefixes, expectedHashPrefixes, "Hash prefixes should match the expected set after update.") - } - - func testRevision3AddsAndDeletesNothing() async { - let expectedFilterSet = dataStore.filterSet - let expectedHashPrefixes = dataStore.hashPrefixes - - // Save revision and update the filter set and hash prefixes - dataStore.saveRevision(3) - await updateManager.updateFilterSet() - await updateManager.updateHashPrefixes() - - XCTAssertEqual(dataStore.filterSet, expectedFilterSet, "Filter set should match the expected set after update.") - XCTAssertEqual(dataStore.hashPrefixes, expectedHashPrefixes, "Hash prefixes should match the expected set after update.") - } - - func testRevision4AddsAndDeletesData() async { - let expectedFilterSet: Set = [ - Filter(hashValue: "testhash2", regex: ".*test.*"), - Filter(hashValue: "testhash1", regex: ".*example.*"), - Filter(hashValue: "testhash5", regex: ".*test.*") - ] - let expectedHashPrefixes: Set = [ - "a379a6f6", - "dd00ee11", - "cc00dd11", - "bb00cc11" - ] - - // Save revision and update the filter set and hash prefixes - dataStore.saveRevision(4) - await updateManager.updateFilterSet() - await updateManager.updateHashPrefixes() - - XCTAssertEqual(dataStore.filterSet, expectedFilterSet, "Filter set should match the expected set after update.") - XCTAssertEqual(dataStore.hashPrefixes, expectedHashPrefixes, "Hash prefixes should match the expected set after update.") - } -} - -class MockPhishingFileStorageManager: FileStorageManager { - private var data: [String: Data] = [:] - - func write(data: Data, to filename: String) { - self.data[filename] = data - } - - func read(from filename: String) -> Data? { - return data[filename] - } -} diff --git a/Tests/PhishingDetectionTests/PhishingDetectorTests.swift b/Tests/PhishingDetectionTests/PhishingDetectorTests.swift deleted file mode 100644 index d2ef4a02e..000000000 --- a/Tests/PhishingDetectionTests/PhishingDetectorTests.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// PhishingDetectorTests.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 XCTest -@testable import PhishingDetection - -class IsMaliciousTests: XCTestCase { - - private var mockAPIClient: MockPhishingDetectionClient! - private var mockDataStore: MockPhishingDetectionDataStore! - private var mockEventMapping: MockEventMapping! - private var detector: PhishingDetector! - - override func setUp() { - super.setUp() - mockAPIClient = MockPhishingDetectionClient() - mockDataStore = MockPhishingDetectionDataStore() - mockEventMapping = MockEventMapping() - detector = PhishingDetector(apiClient: mockAPIClient, dataStore: mockDataStore, eventMapping: mockEventMapping) - } - - override func tearDown() { - mockAPIClient = nil - mockDataStore = nil - mockEventMapping = nil - detector = nil - super.tearDown() - } - - func testIsMaliciousWithLocalFilterHit() async { - let filter = Filter(hashValue: "255a8a793097aeea1f06a19c08cde28db0eb34c660c6e4e7480c9525d034b16d", regex: ".*malicious.*") - mockDataStore.filterSet = Set([filter]) - mockDataStore.hashPrefixes = Set(["255a8a79"]) - - let url = URL(string: "https://malicious.com/")! - - let result = await detector.isMalicious(url: url) - - XCTAssertTrue(result) - } - - func testIsMaliciousWithApiMatch() async { - mockDataStore.filterSet = Set() - mockDataStore.hashPrefixes = ["a379a6f6"] - - let url = URL(string: "https://example.com/mal")! - - let result = await detector.isMalicious(url: url) - - XCTAssertTrue(result) - } - - func testIsMaliciousWithHashPrefixMatch() async { - let filter = Filter(hashValue: "notamatch", regex: ".*malicious.*") - mockDataStore.filterSet = [filter] - mockDataStore.hashPrefixes = ["4c64eb24"] // matches safe.com - - let url = URL(string: "https://safe.com")! - - let result = await detector.isMalicious(url: url) - - XCTAssertFalse(result) - } - - func testIsMaliciousWithFullHashMatch() async { - // 4c64eb2468bcd3e113b37167e6b819aeccf550f974a6082ef17fb74ca68e823b - let filter = Filter(hashValue: "4c64eb2468bcd3e113b37167e6b819aeccf550f974a6082ef17fb74ca68e823b", regex: "https://safe.com/maliciousURI") - mockDataStore.filterSet = [filter] - mockDataStore.hashPrefixes = ["4c64eb24"] - - let url = URL(string: "https://safe.com")! - - let result = await detector.isMalicious(url: url) - - XCTAssertFalse(result) - } - - func testIsMaliciousWithNoHashPrefixMatch() async { - let filter = Filter(hashValue: "testHash", regex: ".*malicious.*") - mockDataStore.filterSet = [filter] - mockDataStore.hashPrefixes = ["testPrefix"] - - let url = URL(string: "https://safe.com")! - - let result = await detector.isMalicious(url: url) - - XCTAssertFalse(result) - } -} diff --git a/Tests/PrivacyDashboardTests/PrivacyDashboardControllerTests.swift b/Tests/PrivacyDashboardTests/PrivacyDashboardControllerTests.swift index 4c03464fa..867e7b888 100644 --- a/Tests/PrivacyDashboardTests/PrivacyDashboardControllerTests.swift +++ b/Tests/PrivacyDashboardTests/PrivacyDashboardControllerTests.swift @@ -260,13 +260,13 @@ final class PrivacyDashboardControllerTests: XCTestCase { func testWhenIsPhishingSetThenJavaScriptEvaluatedWithCorrectString() { let expectation = XCTestExpectation() - let privacyInfo = PrivacyInfo(url: URL(string: "someurl.com")!, parentEntity: nil, protectionStatus: .init(unprotectedTemporary: false, enabledFeatures: [], allowlisted: true, denylisted: true), isPhishing: false) + let privacyInfo = PrivacyInfo(url: URL(string: "someurl.com")!, parentEntity: nil, protectionStatus: .init(unprotectedTemporary: false, enabledFeatures: [], allowlisted: true, denylisted: true), malicousSiteThreatKind: .none) makePrivacyDashboardController(entryPoint: .dashboard, privacyInfo: privacyInfo) let config = WKWebViewConfiguration() let mockWebView = MockWebView(frame: .zero, configuration: config, expectation: expectation) privacyDashboardController.webView = mockWebView - privacyDashboardController.privacyInfo!.isPhishing = true + privacyDashboardController.privacyInfo!.malicousSiteThreatKind = .phishing wait(for: [expectation], timeout: 100) XCTAssertEqual(mockWebView.capturedJavaScriptString, "window.onChangePhishingStatus({\"phishingStatus\":true})")