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})")