Skip to content

Commit

Permalink
Tespach/phishing detection tests (#3222)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1208196336229421/f
Tech Design URL:
CC:

**Description**:
Implement test cases for phishing detection error page, tab extension,
and privacy dashboard.

**Steps to test this PR**:
1. Run the UnitTests + IntegrationTests
2. Visit https://privacy-test-pages.site/security/badware/phishing.html
3. Ensure warning is thrown
4. Click through warning

<!--
Tagging instructions
If this PR isn't ready to be merged for whatever reason it should be
marked with the `DO NOT MERGE` label (particularly if it's a draft)
If it's pending Product Review/PFR, please add the `Pending Product
Review` label.

If at any point it isn't actively being worked on/ready for
review/otherwise moving forward (besides the above PR/PFR exception)
strongly consider closing it (or not opening it in the first place). If
you decide not to close it, make sure it's labelled to make it clear the
PRs state and comment with more information.
-->

**Definition of Done**:

* [ ] Does this PR satisfy our [Definition of
Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)?

---
###### Internal references:
[Pull Request Review
Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f)
[Software Engineering
Expectations](https://app.asana.com/0/59792373528535/199064865822552)
[Technical Design
Template](https://app.asana.com/0/59792373528535/184709971311943)
[Pull Request
Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f)
  • Loading branch information
not-a-rootkit authored Sep 9, 2024
1 parent b939d61 commit 1c75030
Show file tree
Hide file tree
Showing 9 changed files with 505 additions and 16 deletions.
22 changes: 20 additions & 2 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2703,6 +2703,10 @@
CD34F0C12C886482006826BE /* PhishingDetectionMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD34F0BF2C886482006826BE /* PhishingDetectionMocks.swift */; };
CD34F0C22C886482006826BE /* PhishingDetectionMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD34F0BF2C886482006826BE /* PhishingDetectionMocks.swift */; };
CD34F0C42C8869FF006826BE /* PhishingDetection in Frameworks */ = {isa = PBXBuildFile; productRef = CD34F0C32C8869FF006826BE /* PhishingDetection */; };
CD89DD612C89E08D0080F9AF /* PhishingDetectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD89DD5D2C89E08D0080F9AF /* PhishingDetectionTests.swift */; };
CD89DD622C89E08D0080F9AF /* PhishingDetectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD89DD5D2C89E08D0080F9AF /* PhishingDetectionTests.swift */; };
CD89DD652C89E0BB0080F9AF /* PhishingDetectionIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD89DD632C89E0BB0080F9AF /* PhishingDetectionIntegrationTests.swift */; };
CD89DD662C89E0BB0080F9AF /* PhishingDetectionIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD89DD632C89E0BB0080F9AF /* PhishingDetectionIntegrationTests.swift */; };
D64A5FF82AEA5C2B00B6D6E7 /* HomeButtonMenuFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A5FF72AEA5C2B00B6D6E7 /* HomeButtonMenuFactory.swift */; };
D64A5FF92AEA5C2B00B6D6E7 /* HomeButtonMenuFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A5FF72AEA5C2B00B6D6E7 /* HomeButtonMenuFactory.swift */; };
D6BC8AC62C5A95AA0025375B /* DuckPlayer in Frameworks */ = {isa = PBXBuildFile; productRef = D6BC8AC52C5A95AA0025375B /* DuckPlayer */; };
Expand Down Expand Up @@ -4408,6 +4412,8 @@
CD33012B2C89B588009AA127 /* ErrorPageHTMLFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorPageHTMLFactory.swift; sourceTree = "<group>"; };
CD33012F2C89B602009AA127 /* ErrorPageHTMLFactoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorPageHTMLFactoryTests.swift; sourceTree = "<group>"; };
CD34F0BF2C886482006826BE /* PhishingDetectionMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhishingDetectionMocks.swift; sourceTree = "<group>"; };
CD89DD5D2C89E08D0080F9AF /* PhishingDetectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhishingDetectionTests.swift; sourceTree = "<group>"; };
CD89DD632C89E0BB0080F9AF /* PhishingDetectionIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhishingDetectionIntegrationTests.swift; sourceTree = "<group>"; };
CDE248A32C821FFE00F9399D /* hashPrefixes.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = hashPrefixes.json; sourceTree = "<group>"; };
CDE248A42C821FFE00F9399D /* PhishingDetection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhishingDetection.swift; sourceTree = "<group>"; };
CDE248A52C821FFE00F9399D /* PhishingDetectionPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhishingDetectionPreferences.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4717,7 +4723,6 @@
EE7295E32A545B9A008C0991 /* NetworkProtection in Frameworks */,
9807F645278CA16F00E1547B /* BrowserServicesKit in Frameworks */,
D6BC8AC62C5A95AA0025375B /* DuckPlayer in Frameworks */,
CDE248A02C821DF400F9399D /* PhishingDetection in Frameworks */,
987799ED299998B1005D8EB6 /* Bookmarks in Frameworks */,
1E950E3F2912A10D0051A99B /* ContentBlocking in Frameworks */,
31A3A4E32B0C115F0021063C /* DataBrokerProtection in Frameworks */,
Expand Down Expand Up @@ -5280,7 +5285,6 @@
isa = PBXGroup;
children = (
9D9DE5712C63A96400D20B15 /* AppKitExtensions */,
CDE2489E2C821DE800F9399D /* BrowserServicesKit */,
7B9167A82C09E88800322310 /* AppLauncher */,
378E279D2970217400FCADA2 /* BuildToolPlugins */,
3192A2702A4C4E330084EA89 /* DataBrokerProtection */,
Expand Down Expand Up @@ -5485,6 +5489,7 @@
B62A233E29C41D2D00D22475 /* History */,
B603973229BEF84900902A34 /* HTTPSUpgrade */,
B62A233A29C322A000D22475 /* NavigationProtection */,
CD89DD642C89E0BB0080F9AF /* PhishingDetection */,
B603973629BF0E9400902A34 /* PrivacyDashboard */,
B644B43C29D56811003FA9AB /* Tab */,
4B1AD91625FC46FB00261379 /* CoreDataEncryptionTests.swift */,
Expand Down Expand Up @@ -8783,6 +8788,7 @@
isa = PBXGroup;
children = (
CD34F0C02C886482006826BE /* Mocks */,
CD89DD5D2C89E08D0080F9AF /* PhishingDetectionTests.swift */,
);
path = PhishingDetection;
sourceTree = "<group>";
Expand All @@ -8795,6 +8801,14 @@
path = Mocks;
sourceTree = "<group>";
};
CD89DD642C89E0BB0080F9AF /* PhishingDetection */ = {
isa = PBXGroup;
children = (
CD89DD632C89E0BB0080F9AF /* PhishingDetectionIntegrationTests.swift */,
);
path = PhishingDetection;
sourceTree = "<group>";
};
CDE248A22C821FD500F9399D /* PhishingDetection */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -11337,6 +11351,7 @@
3706FE75293F661700E42796 /* WebsiteBreakageReportTests.swift in Sources */,
56D145EF29E6DAD900E3488A /* DataImportProviderTests.swift in Sources */,
569277C529DEE09D00B633EF /* ContinueSetUpModelTests.swift in Sources */,
CD89DD622C89E08D0080F9AF /* PhishingDetectionTests.swift in Sources */,
3706FE76293F661700E42796 /* MockSecureVault.swift in Sources */,
F1AFDBD92C23221700710F2C /* SubscriptionAppStoreRestorerTests.swift in Sources */,
C1E961F32B87B273001760E1 /* MockAutofillActionExecutor.swift in Sources */,
Expand Down Expand Up @@ -11407,6 +11422,7 @@
3706FEA5293F662100E42796 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */,
B603973D29BF1D7D00902A34 /* AutoconsentIntegrationTests.swift in Sources */,
B60C6F8729B1CAB2007BFAA8 /* TestRunHelper.swift in Sources */,
CD89DD662C89E0BB0080F9AF /* PhishingDetectionIntegrationTests.swift in Sources */,
B64CE01F2B8622D700126CA5 /* AddressBarTests.swift in Sources */,
B603972D29BEDF2100902A34 /* ExpectedNavigationExtension.swift in Sources */,
3706FEA6293F662100E42796 /* EncryptionKeyStoreTests.swift in Sources */,
Expand Down Expand Up @@ -11451,6 +11467,7 @@
B644B43D29D56829003FA9AB /* SearchNonexistentDomainTests.swift in Sources */,
B603973C29BF1D7D00902A34 /* AutoconsentIntegrationTests.swift in Sources */,
B60C6F8629B1CAB0007BFAA8 /* TestRunHelper.swift in Sources */,
CD89DD652C89E0BB0080F9AF /* PhishingDetectionIntegrationTests.swift in Sources */,
B64CE01E2B8622D700126CA5 /* AddressBarTests.swift in Sources */,
B603972C29BEDF2100902A34 /* ExpectedNavigationExtension.swift in Sources */,
4B1AD8D525FC38DD00261379 /* EncryptionKeyStoreTests.swift in Sources */,
Expand Down Expand Up @@ -12743,6 +12760,7 @@
857E5AFA2A7961FF00FC0FB4 /* PixelExperimentTests.swift in Sources */,
AAC9C01524CAFBCE00AD1325 /* TabTests.swift in Sources */,
B69B504C2726CA2900758A2B /* MockVariantManager.swift in Sources */,
CD89DD612C89E08D0080F9AF /* PhishingDetectionTests.swift in Sources */,
310E79BF294A19A8007C49E8 /* FireproofingReferenceTests.swift in Sources */,
B6BBF1722744CE36004F850E /* FireproofDomainsStoreMock.swift in Sources */,
4BA1A6D9258C0CB300F6F690 /* DataEncryptionTests.swift in Sources */,
Expand Down
34 changes: 26 additions & 8 deletions DuckDuckGo/PhishingDetection/PhishingDetection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class PhishingDetection: PhishingSiteDetecting {
private var detectionPreferences: PhishingDetectionPreferences
private var dataStore: PhishingDetectionDataSaving
private var featureFlagger: FeatureFlagger
private var config: PrivacyConfiguration
private var configManager: PrivacyConfigurationManaging
private var cancellable: AnyCancellable?
private let revision: Int
private let filterSetURL: URL
Expand All @@ -59,14 +59,16 @@ public class PhishingDetection: PhishingSiteDetecting {
updateManager: PhishingDetectionUpdateManaging? = nil,
dataActivities: PhishingDetectionDataActivityHandling? = nil,
detectionPreferences: PhishingDetectionPreferences = PhishingDetectionPreferences.shared,
featureFlagger: FeatureFlagger? = nil
featureFlagger: FeatureFlagger? = nil,
configManager: PrivacyConfigurationManaging? = nil
) {
self.revision = revision
self.filterSetURL = filterSetURL
self.filterSetDataSHA = filterSetDataSHA
self.hashPrefixURL = hashPrefixURL
self.hashPrefixDataSHA = hashPrefixDataSHA
self.featureFlagger = featureFlagger ?? NSApp.delegateTyped.featureFlagger
self.configManager = configManager ?? AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager

let resolvedDependencies = PhishingDetection.resolveDependencies(
revision: revision,
Expand All @@ -87,7 +89,6 @@ public class PhishingDetection: PhishingSiteDetecting {
self.updateManager = resolvedDependencies.updateManager
self.dataActivities = resolvedDependencies.dataActivities
self.detectionPreferences = detectionPreferences
self.config = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager.privacyConfig

self.startUpdateTasksIfEnabled()
self.setupBindings()
Expand All @@ -96,12 +97,28 @@ public class PhishingDetection: PhishingSiteDetecting {
convenience init(
dataActivities: PhishingDetectionDataActivityHandling,
dataStore: PhishingDetectionDataSaving,
detector: PhishingDetecting
detector: PhishingDetecting,
configManager: PrivacyConfigurationManaging = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager
) {
self.init(
dataStore: dataStore,
detector: detector,
dataActivities: dataActivities
dataStore: dataStore, detector: detector, dataActivities: dataActivities,
detectionPreferences: PhishingDetectionPreferences.shared,
featureFlagger: NSApp.delegateTyped.featureFlagger,
configManager: configManager
)
}

convenience init(featureFlagger: FeatureFlagger, configManager: PrivacyConfigurationManaging) {
self.init(
detectionClient: PhishingDetectionAPIClient(),
dataProvider: nil,
dataStore: nil,
detector: nil,
updateManager: nil,
dataActivities: nil,
detectionPreferences: PhishingDetectionPreferences.shared,
featureFlagger: featureFlagger,
configManager: configManager
)
}

Expand Down Expand Up @@ -131,6 +148,7 @@ public class PhishingDetection: PhishingSiteDetecting {
let resolvedDetector = detector ?? PhishingDetector(apiClient: detectionClient, dataStore: resolvedDataStore)
let resolvedUpdateManager = updateManager ?? PhishingDetectionUpdateManager(client: detectionClient, dataStore: resolvedDataStore)
let resolvedDataActivities = dataActivities ?? PhishingDetectionDataActivities(phishingDetectionDataProvider: resolvedDataProvider, updateManager: resolvedUpdateManager)

return (resolvedDataStore, resolvedDetector, resolvedUpdateManager, resolvedDataActivities)
}

Expand All @@ -148,7 +166,7 @@ public class PhishingDetection: PhishingSiteDetecting {
}

public func checkIsMaliciousIfEnabled(url: URL) async -> Bool {
if config.isFeature(.phishingDetection, enabledForDomain: url.host),
if configManager.privacyConfig.isFeature(.phishingDetection, enabledForDomain: url.host),
detectionPreferences.isEnabled {
return await detector.isMalicious(url: url)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ extension SpecialErrorPageTabExtension: NavigationResponder {
errorData = SpecialErrorData(kind: .phishing, domain: domain, eTldPlus1: tld.eTLDplus1(failingURL?.host))
if let errorURL = generateErrorPageURL(url) {
_ = webView?.load(URLRequest(url: errorURL))
return .cancel
return .none
}
} else {
return handleMaliciousIframe(navigationAction: navigationAction)
Expand All @@ -152,7 +152,7 @@ extension SpecialErrorPageTabExtension: NavigationResponder {

if let errorURL = generateErrorPageURL(iframeTopUrl) {
_ = webView?.load(URLRequest(url: errorURL))
return .cancel
return .none
}

return .next
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
//
// PhishingDetectionIntegrationTests.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 Combine
import Common
import XCTest
import BrowserServicesKit
import PhishingDetection

@testable import DuckDuckGo_Privacy_Browser

@available(macOS 12.0, *)
class PhishingDetectionIntegrationTests: XCTestCase {

var window: NSWindow!
var cancellables: Set<AnyCancellable>!
var phishingDetector: PhishingSiteDetecting!
var tab: Tab!
var tabViewModel: TabViewModel!

@MainActor
override func setUp() {
super.setUp()
WebTrackingProtectionPreferences.shared.isGPCEnabled = false
PhishingDetectionPreferences.shared.isEnabled = true
let featureFlagger = MockFeatureFlagger()
phishingDetector = PhishingDetection(featureFlagger: featureFlagger, configManager: MockPrivacyConfigurationManager())
tab = Tab(content: .none, phishingDetector: phishingDetector)
tabViewModel = TabViewModel(tab: tab)
window = WindowsManager.openNewWindow(with: tab)!
cancellables = Set<AnyCancellable>()
}

@MainActor
override func tearDown() async throws {
window.close()
window = nil
cancellables = nil
phishingDetector = nil
tab = nil
tabViewModel = nil
WebTrackingProtectionPreferences.shared.isGPCEnabled = true
try await super.tearDown()
}

// MARK: - Tests

@MainActor
func testPhishingNotDetected_tabIsNotMarkedPhishing() async throws {
try await loadUrl("http://privacy-test-pages.site/")
let tabErrorCode2 = tabViewModel.tab.error?.errorCode
XCTAssertNil(tabErrorCode2)
}

@MainActor
func testPhishingDetected_tabIsMarkedPhishing() async throws {
try await loadUrl("http://privacy-test-pages.site/security/badware/phishing.html")
let url = URL(string: "http://privacy-test-pages.site/security/badware/phishing.html")!
try await waitForTabToFinishLoading()
let tabErrorCode = tabViewModel.tab.error?.errorCode
XCTAssertEqual(tabErrorCode, PhishingDetectionError.detected.errorCode)
}

@MainActor
func testFeatureDisabledAndPhishingDetection_tabIsNotMarkedPhishing() async throws {
PhishingDetectionPreferences.shared.isEnabled = false
try await loadUrl("http://privacy-test-pages.site/security/badware/phishing.html")
let url = URL(string: "http://privacy-test-pages.site/security/badware/phishing.html")!
try await waitForTabToFinishLoading()
let tabErrorCode = tabViewModel.tab.error?.errorCode
XCTAssertNil(tabErrorCode)
}

@MainActor
func testPhishingDetectedThenNotDetected_tabIsNotMarkedPhishing() async throws {
try await loadUrl("http://privacy-test-pages.site/security/badware/phishing.html")
try await waitForTabToFinishLoading()
let tabErrorCode = tabViewModel.tab.error?.errorCode
XCTAssertEqual(tabErrorCode, PhishingDetectionError.detected.errorCode)

try await loadUrl("http://broken.third-party.site/")
try await waitForTabToFinishLoading()
let tabErrorCode2 = tabViewModel.tab.error?.errorCode
XCTAssertNil(tabErrorCode2)
}

@MainActor
func testPhishingDetectedThenDDGLoaded_tabIsNotMarkedPhishing() async throws {
try await loadUrl("http://privacy-test-pages.site/security/badware/phishing.html")
try await waitForTabToFinishLoading()
let tabErrorCode = tabViewModel.tab.error?.errorCode
XCTAssertEqual(tabErrorCode, PhishingDetectionError.detected.errorCode)

try await loadUrl("http://duckduckgo.com/")
try await waitForTabToFinishLoading()
let tabErrorCode2 = tabViewModel.tab.error?.errorCode
XCTAssertNil(tabErrorCode2)
}

@MainActor
func testPhishingDetectedViaHTTPRedirectChain_tabIsMarkedPhishing() async throws {
try await loadUrl("http://bad.third-party.site/security/badware/phishing-redirect/")
try await waitForTabToFinishLoading()
let tabErrorCode = tabViewModel.tab.error?.errorCode
XCTAssertEqual(tabErrorCode, PhishingDetectionError.detected.errorCode)
}

@MainActor
func testPhishingDetectedRepeatedClientRedirectChains_tabIsMarkedPhishing() async throws {
let urls = [
"http://privacy-test-pages.site/security/badware/phishing-js-redirector-helper.html",
"http://privacy-test-pages.site/security/badware/phishing-js-redirector.html",
"http://privacy-test-pages.site/security/badware/phishing-meta-redirect.html",
]

for url in urls {
try await loadUrl(url)
try await waitForTabToFinishLoading()
let tabErrorCode = tabViewModel.tab.error?.errorCode
XCTAssertEqual(tabErrorCode, PhishingDetectionError.detected.errorCode)
}
}

@MainActor
func testPhishingDetectedRepeatedServerRedirectChains_tabIsMarkedPhishing() async throws {
let urls = [
"http://privacy-test-pages.site/security/badware/phishing-redirect/302",
"http://privacy-test-pages.site/security/badware/phishing-redirect/js",
"http://privacy-test-pages.site/security/badware/phishing-redirect/meta",
"http://privacy-test-pages.site/security/badware/phishing-redirect/meta2"
]

for url in urls {
try await loadUrl(url)
try await waitForTabToFinishLoading()
let tabErrorCode = tabViewModel.tab.error?.errorCode
XCTAssertEqual(tabErrorCode, PhishingDetectionError.detected.errorCode)
}
}

// MARK: - Helper Methods

@MainActor
private func loadUrl(_ urlString: String) async throws {
guard let url = URL(string: urlString) else { return }
_ = await tabViewModel.tab.setUrl(url, source: .link)?.result
}

@MainActor
func waitForTabToFinishLoading() async throws {
let loadingExpectation = expectation(description: "Tab finished loading")
Task {
while tabViewModel.tab.isLoading {
await Task.yield()
}
loadingExpectation.fulfill()
}
await fulfillment(of: [loadingExpectation], timeout: 5)
}
}

class MockFeatureFlagger: FeatureFlagger {
func isFeatureOn<F>(forProvider: F) -> Bool where F: BrowserServicesKit.FeatureFlagSourceProviding {
return true
}
}
Loading

0 comments on commit 1c75030

Please sign in to comment.