| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| #include <optional> |
| #include <tuple> |
| #include <utility> |
| |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/read_only_shared_memory_region.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/run_loop.h" |
| #include "base/synchronization/waitable_event.h" |
| #include "base/test/gmock_move_support.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_command_line.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/simple_test_tick_clock.h" |
| #include "chrome/browser/safe_browsing/chrome_client_side_detection_host_delegate.h" |
| #include "chrome/browser/safe_browsing/chrome_safe_browsing_blocking_page_factory.h" |
| #include "chrome/browser/safe_browsing/chrome_ui_manager_delegate.h" |
| #include "chrome/browser/safe_browsing/safe_browsing_service.h" |
| #include "chrome/browser/safe_browsing/verdict_cache_manager_factory.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/common/url_constants.h" |
| #include "chrome/test/base/chrome_render_view_host_test_harness.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "components/permissions/test/mock_permission_prompt_factory.h" |
| #include "components/permissions/test/mock_permission_request.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "components/safe_browsing/content/browser/async_check_tracker.h" |
| #include "components/safe_browsing/content/browser/client_side_detection_feature_cache.h" |
| #include "components/safe_browsing/content/browser/client_side_detection_service.h" |
| #include "components/safe_browsing/content/browser/client_side_phishing_model.h" |
| #include "components/safe_browsing/content/browser/content_unsafe_resource_util.h" |
| #include "components/safe_browsing/content/browser/ui_manager.h" |
| #include "components/safe_browsing/content/browser/url_checker_holder.h" |
| #include "components/safe_browsing/content/common/safe_browsing.mojom-shared.h" |
| #include "components/safe_browsing/core/browser/db/database_manager.h" |
| #include "components/safe_browsing/core/browser/db/hit_report.h" |
| #include "components/safe_browsing/core/browser/db/test_database_manager.h" |
| #include "components/safe_browsing/core/browser/db/v4_protocol_manager_util.h" |
| #include "components/safe_browsing/core/browser/sync/sync_utils.h" |
| #include "components/safe_browsing/core/common/features.h" |
| #include "components/safe_browsing/core/common/proto/csd.pb.h" |
| #include "components/safe_browsing/core/common/safe_browsing_prefs.h" |
| #include "components/security_interstitials/core/unsafe_resource.h" |
| #include "components/signin/public/identity_manager/identity_test_environment.h" |
| #include "content/public/browser/back_forward_cache.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/mock_render_process_host.h" |
| #include "content/public/test/navigation_simulator.h" |
| #include "content/public/test/test_renderer_host.h" |
| #include "content/public/test/web_contents_tester.h" |
| #include "ipc/ipc_test_sink.h" |
| #include "mojo/public/cpp/base/proto_wrapper.h" |
| #include "mojo/public/cpp/bindings/associated_receiver_set.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "net/http/http_status_code.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" |
| #include "url/gurl.h" |
| |
| using content::BrowserThread; |
| using content::RenderFrameHostTester; |
| using content::WebContents; |
| using ::testing::_; |
| using ::testing::DeleteArg; |
| using ::testing::DoAll; |
| using ::testing::Eq; |
| using ::testing::Invoke; |
| using ::testing::IsNull; |
| using ::testing::Mock; |
| using ::testing::NiceMock; |
| using ::testing::NotNull; |
| using ::testing::Pointee; |
| using ::testing::Return; |
| using ::testing::ReturnRef; |
| using ::testing::SaveArg; |
| using ::testing::SetArgPointee; |
| using ::testing::StrictMock; |
| |
| namespace { |
| |
| const bool kFalse = false; |
| const bool kTrue = true; |
| |
| std::unique_ptr<content::NavigationSimulator> NavigateAndKeepLoading( |
| content::WebContents* web_contents, |
| const GURL& url) { |
| auto navigation = |
| content::NavigationSimulator::CreateBrowserInitiated(url, web_contents); |
| navigation->SetKeepLoading(true); |
| navigation->Commit(); |
| return navigation; |
| } |
| |
| } // namespace |
| |
| namespace safe_browsing { |
| namespace { |
| |
| class MockSafeBrowsingTokenFetcher : public SafeBrowsingTokenFetcher { |
| public: |
| MockSafeBrowsingTokenFetcher() = default; |
| |
| MockSafeBrowsingTokenFetcher(const MockSafeBrowsingTokenFetcher&) = delete; |
| MockSafeBrowsingTokenFetcher& operator=(const MockSafeBrowsingTokenFetcher&) = |
| delete; |
| |
| ~MockSafeBrowsingTokenFetcher() override = default; |
| |
| MOCK_METHOD1(Start, void(Callback)); |
| MOCK_METHOD1(OnInvalidAccessToken, void(const std::string&)); |
| }; |
| |
| // This matcher verifies that the client computed verdict |
| // (ClientPhishingRequest) which is passed to SendClientReportPhishingRequest |
| // has the expected fields set. Note: we can't simply compare the protocol |
| // buffer strings because the BrowserFeatureExtractor might add features to the |
| // verdict object before calling SendClientReportPhishingRequest. |
| MATCHER_P(PartiallyEqualVerdict, other, "") { |
| return (other.url() == arg->url() && |
| other.client_score() == arg->client_score() && |
| other.is_phishing() == arg->is_phishing()); |
| } |
| |
| MATCHER_P(HasScamThreatSubtype, other, "") { |
| return (other.threat_subtype == arg.threat_subtype); |
| } |
| |
| // Test that the callback is nullptr when the verdict is not phishing. |
| MATCHER(CallbackIsNull, "") { |
| return arg.is_null(); |
| } |
| |
| class MockClientSideDetectionService : public ClientSideDetectionService { |
| public: |
| MockClientSideDetectionService() |
| : ClientSideDetectionService(nullptr, nullptr) {} |
| |
| MockClientSideDetectionService(const MockClientSideDetectionService&) = |
| delete; |
| MockClientSideDetectionService& operator=( |
| const MockClientSideDetectionService&) = delete; |
| |
| ~MockClientSideDetectionService() override = default; |
| |
| MOCK_METHOD3(SendClientReportPhishingRequest, |
| void(std::unique_ptr<ClientPhishingRequest>, |
| ClientReportPhishingRequestCallback, |
| const std::string&)); |
| MOCK_CONST_METHOD1(IsPrivateIPAddress, bool(const net::IPAddress&)); |
| MOCK_CONST_METHOD1(IsLocalResource, bool(const net::IPAddress&)); |
| MOCK_METHOD2(GetValidCachedResult, bool(const GURL&, bool*)); |
| MOCK_METHOD0(AtPhishingReportLimit, bool()); |
| MOCK_METHOD0(GetModelSharedMemoryRegion, base::ReadOnlySharedMemoryRegion()); |
| MOCK_METHOD0(GetModelType, CSDModelType()); |
| MOCK_METHOD0(IsModelAvailable, bool()); |
| MOCK_METHOD( |
| void, |
| InquireOnDeviceModel, |
| (std::string, |
| base::OnceCallback<void( |
| std::optional<optimization_guide::proto::ScamDetectionResponse>)>)); |
| MOCK_METHOD0(LogOnDeviceModelEligibilityReason, void()); |
| }; |
| |
| class MockSafeBrowsingUIManager : public SafeBrowsingUIManager { |
| public: |
| MockSafeBrowsingUIManager() |
| : SafeBrowsingUIManager( |
| std::make_unique<ChromeSafeBrowsingUIManagerDelegate>(), |
| std::make_unique<ChromeSafeBrowsingBlockingPageFactory>(), |
| GURL(chrome::kChromeUINewTabURL)) {} |
| |
| MockSafeBrowsingUIManager(const MockSafeBrowsingUIManager&) = delete; |
| MockSafeBrowsingUIManager& operator=(const MockSafeBrowsingUIManager&) = |
| delete; |
| |
| MOCK_METHOD1(DisplayBlockingPage, void(const UnsafeResource& resource)); |
| protected: |
| ~MockSafeBrowsingUIManager() override = default; |
| }; |
| |
| class MockSafeBrowsingDatabaseManager : public TestSafeBrowsingDatabaseManager { |
| public: |
| MockSafeBrowsingDatabaseManager() |
| : safe_browsing::TestSafeBrowsingDatabaseManager( |
| content::GetUIThreadTaskRunner({})) {} |
| |
| MockSafeBrowsingDatabaseManager(const MockSafeBrowsingDatabaseManager&) = |
| delete; |
| MockSafeBrowsingDatabaseManager& operator=( |
| const MockSafeBrowsingDatabaseManager&) = delete; |
| |
| MOCK_METHOD2(CheckCsdAllowlistUrl, |
| AsyncMatch(const GURL&, SafeBrowsingDatabaseManager::Client*)); |
| MOCK_CONST_METHOD1(CanCheckUrl, bool(const GURL&)); |
| |
| // Calls the callback with the allowlist match result previously set by |
| // |SetAllowlistLookupDetailsForUrl|. Returns std::nullopt. It crashes if |
| // the allowlist match result is not set in advance for the |gurl|. |
| void CheckUrlForHighConfidenceAllowlist( |
| const GURL& gurl, |
| CheckUrlForHighConfidenceAllowlistCallback callback) override { |
| std::string url = gurl.spec(); |
| DCHECK(base::Contains(urls_allowlist_match_, url)); |
| |
| ui_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| std::move(callback), |
| /*url_on_high_confidence_allowlist=*/urls_allowlist_match_[url], |
| /*logging_details=*/std::nullopt)); |
| } |
| |
| void SetAllowlistLookupDetailsForUrl(const GURL& gurl, bool match) { |
| std::string url = gurl.spec(); |
| urls_allowlist_match_[url] = match; |
| } |
| |
| protected: |
| ~MockSafeBrowsingDatabaseManager() override = default; |
| |
| private: |
| base::flat_map<std::string, bool> urls_allowlist_match_; |
| }; |
| |
| class MockClientSideDetectionHostDelegate |
| : public ChromeClientSideDetectionHostDelegate { |
| public: |
| explicit MockClientSideDetectionHostDelegate( |
| content::WebContents* web_contents) |
| : ChromeClientSideDetectionHostDelegate(web_contents) {} |
| |
| void GetInnerText(HostInnerTextCallback callback) override { |
| std::move(callback).Run(inner_text_); |
| } |
| |
| void ForceEmptyInnerText() { inner_text_ = ""; } |
| |
| private: |
| std::string inner_text_ = "inner text"; |
| }; |
| |
| } // namespace |
| |
| class FakePhishingDetector : public mojom::PhishingDetector { |
| public: |
| FakePhishingDetector() = default; |
| |
| FakePhishingDetector(const FakePhishingDetector&) = delete; |
| FakePhishingDetector& operator=(const FakePhishingDetector&) = delete; |
| |
| ~FakePhishingDetector() override = default; |
| |
| void BindReceiver(mojo::ScopedInterfaceEndpointHandle handle) { |
| receivers_.Add(this, |
| mojo::PendingAssociatedReceiver<mojom::PhishingDetector>( |
| std::move(handle))); |
| } |
| |
| // mojom::PhishingDetector |
| void StartPhishingDetection( |
| const GURL& url, |
| StartPhishingDetectionCallback callback) override { |
| url_ = url; |
| phishing_detection_started_ = true; |
| |
| // The callback must be run before destruction, so send a minimal |
| // ClientPhishingRequest. |
| ClientPhishingRequest request; |
| request.set_client_score(0.8); |
| std::move(callback).Run(mojom::PhishingDetectorResult::SUCCESS, |
| mojo_base::ProtoWrapper(request)); |
| |
| return; |
| } |
| |
| void CheckMessage(const GURL* url) { |
| if (!url) { |
| EXPECT_FALSE(phishing_detection_started_); |
| } else { |
| ASSERT_TRUE(phishing_detection_started_); |
| EXPECT_EQ(*url, url_); |
| } |
| } |
| |
| void Reset() { |
| phishing_detection_started_ = false; |
| url_ = GURL(); |
| } |
| |
| private: |
| mojo::AssociatedReceiverSet<mojom::PhishingDetector> receivers_; |
| bool phishing_detection_started_ = false; |
| GURL url_; |
| }; |
| |
| class ClientSideDetectionHostTestBase : public ChromeRenderViewHostTestHarness { |
| public: |
| typedef security_interstitials::UnsafeResource UnsafeResource; |
| |
| class WebContentsObserver : public content::WebContentsObserver { |
| public: |
| WebContentsObserver(ClientSideDetectionHostTestBase* harness, |
| content::WebContents* contents) |
| : content::WebContentsObserver(contents), harness_(harness) {} |
| |
| void RenderFrameCreated( |
| content::RenderFrameHost* render_frame_host) override { |
| harness_->InitTestApi(render_frame_host); |
| } |
| |
| private: |
| // The raw pointer is safe because `harness_` owns this. |
| raw_ptr<ClientSideDetectionHostTestBase> harness_; |
| }; |
| |
| explicit ClientSideDetectionHostTestBase(bool is_incognito) |
| : is_incognito_(is_incognito) {} |
| |
| void InitTestApi(content::RenderFrameHost* rfh) { |
| rfh->GetRemoteAssociatedInterfaces()->OverrideBinderForTesting( |
| mojom::PhishingDetector::Name_, |
| base::BindRepeating(&FakePhishingDetector::BindReceiver, |
| base::Unretained(&fake_phishing_detector_))); |
| } |
| |
| void SetUp() override { |
| ChromeRenderViewHostTestHarness::SetUp(); |
| |
| observer_ = std::make_unique<WebContentsObserver>(this, web_contents()); |
| |
| if (is_incognito_) { |
| auto incognito_web_contents = |
| content::WebContentsTester::CreateTestWebContents( |
| profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true), |
| nullptr); |
| SetContents(std::move(incognito_web_contents)); |
| } |
| |
| // Initiate the connection to a (pretend) renderer process. |
| NavigateAndCommit(GURL("about:blank")); |
| |
| InitTestApi(web_contents()->GetPrimaryMainFrame()); |
| |
| // Inject service classes. |
| csd_service_ = std::make_unique<NiceMock<MockClientSideDetectionService>>(); |
| database_manager_ = new NiceMock<MockSafeBrowsingDatabaseManager>(); |
| ui_manager_ = new NiceMock<MockSafeBrowsingUIManager>(); |
| |
| identity_test_env_.MakePrimaryAccountAvailable("user@gmail.com", |
| signin::ConsentLevel::kSync); |
| |
| csd_host_ = |
| ChromeClientSideDetectionHostDelegate::CreateHost(web_contents()); |
| csd_host_->set_client_side_detection_service(csd_service_->GetWeakPtr()); |
| csd_host_->set_ui_manager(ui_manager_.get()); |
| csd_host_->set_database_manager(database_manager_.get()); |
| csd_host_->set_tick_clock_for_testing(&clock_); |
| csd_host_->set_is_off_the_record_for_testing(is_incognito_); |
| csd_host_->set_account_signed_in_for_testing( |
| base::BindRepeating(&safe_browsing::SyncUtils::IsPrimaryAccountSignedIn, |
| identity_test_env_.identity_manager())); |
| auto token_fetcher = |
| std::make_unique<NiceMock<MockSafeBrowsingTokenFetcher>>(); |
| raw_token_fetcher_ = token_fetcher.get(); |
| csd_host_->set_token_fetcher_for_testing(std::move(token_fetcher)); |
| auto delegate = |
| std::make_unique<MockClientSideDetectionHostDelegate>(web_contents()); |
| raw_delegate_ = delegate.get(); |
| csd_host_->set_delegate_for_testing(std::move(delegate)); |
| // Commit to a URL for tests that do not explicitly NavigateAndCommit. |
| // Committing to "about:blank" avoids triggering logic irrelevant for tests. |
| NavigateAndCommit(GURL("about:blank")); |
| |
| testing::DefaultValue<CSDModelType>::Set(CSDModelType::kFlatbuffer); |
| } |
| |
| void TearDown() override { |
| raw_token_fetcher_ = nullptr; |
| raw_delegate_ = nullptr; |
| |
| // Delete the host object on the UI thread and release the |
| // SafeBrowsingService. |
| content::GetUIThreadTaskRunner({})->DeleteSoon(FROM_HERE, |
| csd_host_.release()); |
| |
| // RenderProcessHostCreationObserver expects to be torn down on UI. |
| content::GetUIThreadTaskRunner({})->DeleteSoon(FROM_HERE, |
| csd_service_.release()); |
| database_manager_.reset(); |
| ui_manager_.reset(); |
| base::RunLoop().RunUntilIdle(); |
| |
| ChromeRenderViewHostTestHarness::TearDown(); |
| } |
| |
| void PhishingDetectionDone(std::optional<mojo_base::ProtoWrapper> verdict) { |
| csd_host_->PhishingDetectionDone( |
| ClientSideDetectionType::TRIGGER_MODELS, |
| /*is_sample_ping=*/false, /*did_match_high_confidence_allowlist=*/false, |
| mojom::PhishingDetectorResult::SUCCESS, std::move(verdict)); |
| } |
| |
| void PhishingDetectionDoneWithHighConfidenceAllowlistMatch( |
| std::optional<mojo_base::ProtoWrapper> verdict) { |
| csd_host_->PhishingDetectionDone( |
| ClientSideDetectionType::TRIGGER_MODELS, |
| /*is_sample_ping=*/false, /*did_match_high_confidence_allowlist=*/true, |
| mojom::PhishingDetectorResult::SUCCESS, std::move(verdict)); |
| } |
| |
| void PhishingDetectionError(mojom::PhishingDetectorResult error) { |
| csd_host_->PhishingDetectionDone( |
| ClientSideDetectionType::TRIGGER_MODELS, |
| /*is_sample_ping=*/false, /*did_match_high_confidence_allowlist=*/false, |
| error, std::nullopt); |
| } |
| |
| void ExpectPreClassificationChecks(const GURL& url, |
| const bool* is_private, |
| const bool* match_csd_allowlist, |
| const bool* get_valid_cached_result, |
| const bool* over_phishing_report_limit, |
| const bool* is_local) { |
| if (is_private) { |
| EXPECT_CALL(*csd_service_, IsPrivateIPAddress(_)) |
| .WillOnce(Return(*is_private)); |
| } |
| if (match_csd_allowlist) { |
| EXPECT_CALL(*database_manager_.get(), CheckCsdAllowlistUrl(url, _)) |
| .WillOnce(Return(*match_csd_allowlist ? AsyncMatch::MATCH |
| : AsyncMatch::NO_MATCH)); |
| EXPECT_CALL(*database_manager_.get(), CanCheckUrl(_)) |
| .WillOnce(Return(true)); |
| } else { |
| EXPECT_CALL(*database_manager_.get(), CheckCsdAllowlistUrl(url, _)) |
| .Times(0); |
| } |
| if (get_valid_cached_result) { |
| EXPECT_CALL(*csd_service_, GetValidCachedResult(url, NotNull())) |
| .WillOnce( |
| DoAll(SetArgPointee<1>(true), Return(*get_valid_cached_result))); |
| } |
| if (over_phishing_report_limit) { |
| EXPECT_CALL(*csd_service_, AtPhishingReportLimit()) |
| .WillOnce(Return(*over_phishing_report_limit)); |
| } |
| if (is_local) { |
| EXPECT_CALL(*csd_service_, IsLocalResource(_)) |
| .WillOnce(Return(*is_local)); |
| } |
| } |
| |
| void WaitAndCheckPreClassificationChecks() { |
| // Wait for CheckCsdAllowlist and CheckCache() to be called if at all. |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| EXPECT_TRUE(Mock::VerifyAndClear(ui_manager_.get())); |
| EXPECT_TRUE(Mock::VerifyAndClear(database_manager_.get())); |
| } |
| |
| void NavigateAndCommit(const GURL& safe_url) { |
| controller().LoadURL( |
| safe_url, content::Referrer(), ui::PAGE_TRANSITION_LINK, |
| std::string()); |
| |
| content::WebContentsTester::For(web_contents())->CommitPendingNavigation(); |
| } |
| |
| void AdvanceTimeTickClock(base::TimeDelta delta) { clock_.Advance(delta); } |
| |
| void SetFeatures( |
| const std::vector<base::test::FeatureRef>& enabled_features, |
| const std::vector<base::test::FeatureRef>& disabled_features) { |
| feature_list_.InitWithFeatures(enabled_features, disabled_features); |
| } |
| |
| std::string GetRequestTypeName( |
| ClientSideDetectionType client_side_detection_type) { |
| switch (client_side_detection_type) { |
| case safe_browsing::ClientSideDetectionType:: |
| CLIENT_SIDE_DETECTION_TYPE_UNSPECIFIED: |
| return "Unknown"; |
| case safe_browsing::ClientSideDetectionType::FORCE_REQUEST: |
| return "ForceRequest"; |
| case safe_browsing::ClientSideDetectionType:: |
| NOTIFICATION_PERMISSION_PROMPT: |
| return "NotificationPermissionPrompt"; |
| case safe_browsing::ClientSideDetectionType::TRIGGER_MODELS: |
| return "TriggerModel"; |
| case safe_browsing::ClientSideDetectionType::KEYBOARD_LOCK_REQUESTED: |
| return "KeyboardLockRequested"; |
| case safe_browsing::ClientSideDetectionType::POINTER_LOCK_REQUESTED: |
| return "PointerLockRequested"; |
| case safe_browsing::ClientSideDetectionType::VIBRATION_API: |
| return "VibrationApi"; |
| case safe_browsing::ClientSideDetectionType::FULLSCREEN_API: |
| return "FullscreenApi"; |
| } |
| } |
| |
| protected: |
| std::unique_ptr<ClientSideDetectionHost> csd_host_; |
| std::unique_ptr<NiceMock<MockClientSideDetectionService>> csd_service_; |
| scoped_refptr<NiceMock<MockSafeBrowsingUIManager>> ui_manager_; |
| scoped_refptr<NiceMock<MockSafeBrowsingDatabaseManager>> database_manager_; |
| FakePhishingDetector fake_phishing_detector_; |
| raw_ptr<NiceMock<MockSafeBrowsingTokenFetcher>> raw_token_fetcher_ = nullptr; |
| raw_ptr<MockClientSideDetectionHostDelegate> raw_delegate_ = nullptr; |
| base::SimpleTestTickClock clock_; |
| const bool is_incognito_; |
| signin::IdentityTestEnvironment identity_test_env_; |
| base::test::ScopedFeatureList feature_list_; |
| std::unique_ptr<WebContentsObserver> observer_; |
| }; |
| |
| class ClientSideDetectionHostTest : public ClientSideDetectionHostTestBase { |
| public: |
| ClientSideDetectionHostTest() |
| : ClientSideDetectionHostTestBase(false /*is_incognito*/) {} |
| }; |
| |
| class ClientSideDetectionHostIncognitoTest |
| : public ClientSideDetectionHostTestBase { |
| public: |
| ClientSideDetectionHostIncognitoTest() |
| : ClientSideDetectionHostTestBase(true /*is_incognito*/) {} |
| }; |
| |
| TEST_F(ClientSideDetectionHostTest, PhishingDetectionDoneInvalidVerdict) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| // Case 0: renderer sends an invalid protobuf that we're unable to |
| // parse. This has the same behavior as providing nullopt. |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(_, _, _)).Times(0); |
| PhishingDetectionDone(std::nullopt); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, PhishingDetectionDoneNotPhishing) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| // Case 1: client thinks the page is phishing. The server does not agree. |
| // No interstitial is shown. |
| ClientSideDetectionService::ClientReportPhishingRequestCallback cb; |
| ClientPhishingRequest verdict; |
| verdict.set_url("http://phishingurl.com/"); |
| verdict.set_client_score(1.0f); |
| verdict.set_is_phishing(true); |
| |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest( |
| PartiallyEqualVerdict(verdict), _, _)) |
| .WillOnce(MoveArg<1>(&cb)); |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get())); |
| ASSERT_FALSE(cb.is_null()); |
| |
| // Make sure DisplayBlockingPage is not going to be called. |
| EXPECT_CALL(*ui_manager_.get(), DisplayBlockingPage(_)).Times(0); |
| std::move(cb).Run(GURL(verdict.url()), false, net::HTTP_OK, std::nullopt); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(Mock::VerifyAndClear(ui_manager_.get())); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, PhishingDetectionDoneShowInterstitial) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| base::HistogramTester histogram_tester; |
| |
| // Case 2: client thinks the page is phishing and so does the server. |
| // We show an interstitial. |
| ClientSideDetectionService::ClientReportPhishingRequestCallback cb; |
| GURL phishing_url("http://phishingurl.com/"); |
| ClientPhishingRequest verdict; |
| verdict.set_url(phishing_url.spec()); |
| verdict.set_client_score(1.0f); |
| verdict.set_is_phishing(true); |
| |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest( |
| PartiallyEqualVerdict(verdict), _, _)) |
| .WillOnce(MoveArg<1>(&cb)); |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get())); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| ASSERT_FALSE(cb.is_null()); |
| |
| UnsafeResource resource; |
| EXPECT_CALL(*ui_manager_.get(), DisplayBlockingPage(_)) |
| .WillOnce(SaveArg<0>(&resource)); |
| std::move(cb).Run(phishing_url, true, net::HTTP_OK, std::nullopt); |
| |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(Mock::VerifyAndClear(ui_manager_.get())); |
| EXPECT_EQ(phishing_url, resource.url); |
| EXPECT_EQ(phishing_url, resource.original_url); |
| EXPECT_EQ(SBThreatType::SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING, |
| resource.threat_type); |
| EXPECT_EQ(ThreatSource::CLIENT_SIDE_DETECTION, resource.threat_source); |
| EXPECT_EQ(web_contents(), |
| unsafe_resource_util::GetWebContentsForResource(resource)); |
| EXPECT_TRUE(resource.navigation_id.has_value()); |
| |
| histogram_tester.ExpectUniqueSample( |
| "SBClientPhishing.HighConfidenceAllowlistMatchOnServerVerdictPhishy", |
| false, 1); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, PhishingDetectionDoneMultiplePings) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| // Case 3 & 4: client thinks a page is phishing then navigates to |
| // another page which is also considered phishing by the client |
| // before the server responds with a verdict. After a while the |
| // server responds for both requests with a phishing verdict. Only |
| // a single interstitial is shown for the second URL. |
| ClientSideDetectionService::ClientReportPhishingRequestCallback cb; |
| GURL phishing_url("http://phishingurl.com/"); |
| ClientPhishingRequest verdict; |
| verdict.set_url(phishing_url.spec()); |
| verdict.set_client_score(1.0f); |
| verdict.set_is_phishing(true); |
| |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest( |
| PartiallyEqualVerdict(verdict), _, _)) |
| .WillOnce(MoveArg<1>(&cb)); |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get())); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| ASSERT_FALSE(cb.is_null()); |
| |
| GURL other_phishing_url("http://other_phishing_url.com/bla"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(other_phishing_url, false); |
| ExpectPreClassificationChecks(other_phishing_url, &kFalse, &kFalse, &kFalse, |
| &kFalse, &kFalse); |
| // We navigate away. The callback cb should be revoked. |
| NavigateAndCommit(other_phishing_url); |
| // Wait for the pre-classification checks to finish for other_phishing_url. |
| WaitAndCheckPreClassificationChecks(); |
| |
| ClientSideDetectionService::ClientReportPhishingRequestCallback cb_other; |
| verdict.set_url(other_phishing_url.spec()); |
| verdict.set_client_score(0.8f); |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest( |
| PartiallyEqualVerdict(verdict), _, _)) |
| .WillOnce(MoveArg<1>(&cb_other)); |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get())); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| ASSERT_FALSE(cb_other.is_null()); |
| |
| // We expect that the interstitial is shown for the second phishing URL and |
| // not for the first phishing URL. |
| UnsafeResource resource; |
| EXPECT_CALL(*ui_manager_.get(), DisplayBlockingPage(_)) |
| .WillOnce(SaveArg<0>(&resource)); |
| |
| std::move(cb).Run(phishing_url, true, net::HTTP_OK, |
| std::nullopt); // Should have no effect. |
| std::move(cb_other).Run(other_phishing_url, true, net::HTTP_OK, |
| std::nullopt); // Should show interstitial. |
| |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(Mock::VerifyAndClear(ui_manager_.get())); |
| EXPECT_EQ(other_phishing_url, resource.url); |
| EXPECT_EQ(other_phishing_url, resource.original_url); |
| EXPECT_EQ(SBThreatType::SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING, |
| resource.threat_type); |
| EXPECT_EQ(ThreatSource::CLIENT_SIDE_DETECTION, resource.threat_source); |
| EXPECT_EQ(web_contents(), |
| unsafe_resource_util::GetWebContentsForResource(resource)); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, PhishingDetectionDoneVerdictNotPhishing) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| // Case 5: renderer sends a verdict string that isn't phishing. |
| ClientPhishingRequest verdict; |
| verdict.set_url("http://not-phishing.com/"); |
| verdict.set_client_score(0.1f); |
| verdict.set_is_phishing(false); |
| |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(_, _, _)).Times(0); |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| } |
| |
| TEST_F( |
| ClientSideDetectionHostTest, |
| PhishingDetectionDoneServerModelPhishyAndExistsInHighConfidenceAllowlist) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| base::HistogramTester histogram_tester; |
| |
| // Client thinks the page is phishing and so does the server. |
| // We show an interstitial. |
| ClientSideDetectionService::ClientReportPhishingRequestCallback cb; |
| GURL phishing_url("http://phishingurl.com/"); |
| ClientPhishingRequest verdict; |
| verdict.set_url(phishing_url.spec()); |
| verdict.set_client_score(1.0f); |
| verdict.set_is_phishing(true); |
| |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest( |
| PartiallyEqualVerdict(verdict), _, _)) |
| .WillOnce(MoveArg<1>(&cb)); |
| // Bypass the preclassification check with where the allowlist check occurs, |
| // since this unit test strictly tests post classification allowlist match |
| // check. |
| PhishingDetectionDoneWithHighConfidenceAllowlistMatch( |
| mojo_base::ProtoWrapper(verdict)); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get())); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| ASSERT_FALSE(cb.is_null()); |
| |
| UnsafeResource resource; |
| EXPECT_CALL(*ui_manager_.get(), DisplayBlockingPage(_)) |
| .WillOnce(SaveArg<0>(&resource)); |
| std::move(cb).Run(phishing_url, true, net::HTTP_OK, std::nullopt); |
| |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(Mock::VerifyAndClear(ui_manager_.get())); |
| EXPECT_EQ(phishing_url, resource.url); |
| EXPECT_EQ(phishing_url, resource.original_url); |
| EXPECT_EQ(SBThreatType::SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING, |
| resource.threat_type); |
| EXPECT_EQ(ThreatSource::CLIENT_SIDE_DETECTION, resource.threat_source); |
| EXPECT_EQ(web_contents(), |
| unsafe_resource_util::GetWebContentsForResource(resource)); |
| |
| // Test that the histogram has been logged that the allowlist did exist with |
| // the server model verdict phishy. |
| histogram_tester.ExpectUniqueSample( |
| "SBClientPhishing.ServerModelDetectsPhishing", true, 1); |
| histogram_tester.ExpectUniqueSample( |
| "SBClientPhishing.HighConfidenceAllowlistMatchOnServerVerdictPhishy", |
| true, 1); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| PhishingDetectionDoneVerdictNotPhishingButSBMatchOnNewRVH) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| // When navigating to a different host (thus creating a pending RVH) which |
| // matches regular malware list, and after navigation the renderer sends a |
| // verdict string that isn't phishing, we should still send the report. |
| |
| GURL start_url("http://safe.example.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(start_url, false); |
| ExpectPreClassificationChecks(start_url, &kFalse, &kFalse, &kFalse, &kFalse, |
| &kFalse); |
| NavigateAndCommit(start_url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| // Now navigate to a different host which will have a malware hit before the |
| // navigation commits. |
| GURL url("http://malware-but-not-phishing.com/"); |
| ClientPhishingRequest verdict; |
| verdict.set_url(url.spec()); |
| verdict.set_client_score(0.1f); |
| verdict.set_is_phishing(false); |
| |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, false); |
| ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse, |
| &kFalse); |
| NavigateAndCommit(url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| PhishingDetectionDoneEnhancedProtectionShouldHaveToken) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| SetEnhancedProtectionPrefForTests(profile()->GetPrefs(), true); |
| |
| ClientPhishingRequest verdict; |
| verdict.set_url("http://example.com/"); |
| verdict.set_client_score(1.0f); |
| verdict.set_is_phishing(true); |
| |
| // Set up mock call to csd service. |
| EXPECT_CALL(*csd_service_, |
| SendClientReportPhishingRequest(PartiallyEqualVerdict(verdict), _, |
| "fake_access_token")); |
| |
| // Set up mock call to token fetcher. |
| SafeBrowsingTokenFetcher::Callback cb; |
| EXPECT_CALL(*raw_token_fetcher_, Start(_)).WillOnce(MoveArg<0>(&cb)); |
| |
| // Make the call. |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| |
| // Wait for token fetcher to be called. |
| EXPECT_TRUE(Mock::VerifyAndClear(raw_token_fetcher_)); |
| |
| ASSERT_FALSE(cb.is_null()); |
| std::move(cb).Run("fake_access_token"); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| PhishingDetectionDoneCalledTwiceShouldSucceed) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| SetEnhancedProtectionPrefForTests(profile()->GetPrefs(), true); |
| |
| ClientPhishingRequest verdict; |
| verdict.set_url("http://example.com/"); |
| verdict.set_client_score(1.0f); |
| verdict.set_is_phishing(true); |
| |
| // Set up mock call to csd service. |
| EXPECT_CALL(*csd_service_, |
| SendClientReportPhishingRequest(PartiallyEqualVerdict(verdict), _, |
| "fake_access_token_1")) |
| .Times(1); |
| |
| // Set up mock call to csd service. |
| EXPECT_CALL(*csd_service_, |
| SendClientReportPhishingRequest(PartiallyEqualVerdict(verdict), _, |
| "fake_access_token_2")) |
| .Times(1); |
| |
| // Set up mock call to token fetcher. |
| SafeBrowsingTokenFetcher::Callback cb; |
| EXPECT_CALL(*raw_token_fetcher_, Start(_)) |
| .Times(1) |
| .WillRepeatedly(MoveArg<0>(&cb)); |
| |
| // Make the call. |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| |
| // Wait for token fetcher to be called. |
| EXPECT_TRUE(Mock::VerifyAndClear(raw_token_fetcher_)); |
| |
| ASSERT_FALSE(cb.is_null()); |
| std::move(cb).Run("fake_access_token_1"); |
| |
| // Make the call again. |
| EXPECT_CALL(*raw_token_fetcher_, Start(_)) |
| .Times(1) |
| .WillRepeatedly(MoveArg<0>(&cb)); |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| EXPECT_TRUE(Mock::VerifyAndClear(raw_token_fetcher_)); |
| ASSERT_FALSE(cb.is_null()); |
| std::move(cb).Run("fake_access_token_2"); |
| } |
| |
| TEST_F(ClientSideDetectionHostIncognitoTest, |
| PhishingDetectionDoneIncognitoShouldNotHaveToken) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| SetEnhancedProtectionPrefForTests(profile()->GetPrefs(), true); |
| |
| ClientPhishingRequest verdict; |
| verdict.set_url("http://example.com/"); |
| verdict.set_client_score(1.0f); |
| verdict.set_is_phishing(true); |
| |
| // Set up mock call to csd service. |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest( |
| PartiallyEqualVerdict(verdict), _, "")); |
| |
| // Set up mock call to token fetcher. |
| SafeBrowsingTokenFetcher::Callback cb; |
| EXPECT_CALL(*raw_token_fetcher_, Start(_)).Times(0); |
| |
| // Make the call. |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| PhishingDetectionDoneNoEnhancedProtectionShouldNotHaveToken) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| ClientPhishingRequest verdict; |
| verdict.set_url("http://example.com/"); |
| verdict.set_client_score(1.0f); |
| verdict.set_is_phishing(true); |
| |
| // Set up mock call to csd service. |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest( |
| PartiallyEqualVerdict(verdict), _, "")); |
| |
| // Set up mock call to token fetcher. |
| SafeBrowsingTokenFetcher::Callback cb; |
| EXPECT_CALL(*raw_token_fetcher_, Start(_)).Times(0); |
| |
| // Make the call. |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| } |
| |
| // This test doesn't work because it makes assumption about how |
| // the message loop is run, and those assumptions are wrong when properly |
| // simulating a navigation with browser-side navigations. |
| // TODO(clamy): Fix the test and re-enable. See crbug.com/753357. |
| TEST_F(ClientSideDetectionHostTest, |
| DISABLED_NavigationCancelsShouldClassifyUrl) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| // Test that canceling pending should classify requests works as expected. |
| GURL first_url("http://first.phishy.url.com"); |
| GURL second_url("http://second.url.com/"); |
| // The first few checks are done synchronously so check that they have been |
| // done for the first URL, while the second URL has all the checks done. We |
| // need to manually set up the IsPrivateIPAddress mock since if the same mock |
| // expectation is specified twice, gmock will only use the last instance of |
| // it, meaning the first will never be matched. |
| EXPECT_CALL(*csd_service_, IsPrivateIPAddress(_)) |
| .WillOnce(Return(false)) |
| .WillOnce(Return(false)); |
| ExpectPreClassificationChecks(first_url, nullptr, &kFalse, nullptr, nullptr, |
| nullptr); |
| ExpectPreClassificationChecks(second_url, nullptr, &kFalse, &kFalse, &kFalse, |
| nullptr); |
| |
| NavigateAndCommit(first_url); |
| // Don't flush the message loop, as we want to navigate to a different |
| // url before the final pre-classification checks are run. |
| NavigateAndCommit(second_url); |
| WaitAndCheckPreClassificationChecks(); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, TestPreClassificationCheckPass) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| base::HistogramTester histogram_tester; |
| |
| // Navigate the tab to a page. We should see a StartPhishingDetection IPC. |
| GURL url("http://host.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, false); |
| ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse, |
| &kFalse); |
| NavigateAndKeepLoading(web_contents(), url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| fake_phishing_detector_.CheckMessage(&url); |
| |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.PreClassificationCheckResult", |
| PreClassificationCheckResult::CLASSIFY, 1); |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.PreClassificationCheckResult.TriggerModel", |
| PreClassificationCheckResult::CLASSIFY, 1); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| TestPreClassificationCheckMatchCSDAllowlist) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| GURL url("http://host.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, false); |
| ExpectPreClassificationChecks(url, &kFalse, &kTrue, nullptr, nullptr, |
| &kFalse); |
| NavigateAndKeepLoading(web_contents(), url); |
| WaitAndCheckPreClassificationChecks(); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| TestPreClassificationCheckMatchHighConfidenceAllowlist) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| std::vector<base::test::FeatureRef> enabled_features = {}; |
| enabled_features.push_back(kClientSideDetectionAcceptHCAllowlist); |
| SetFeatures(enabled_features, {}); |
| |
| csd_host_->set_high_confidence_allowlist_acceptance_rate_for_testing(1.0f); |
| base::HistogramTester histogram_tester; |
| |
| GURL url("http://host.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, /*match=*/true); |
| ExpectPreClassificationChecks(url, &kFalse, &kFalse, nullptr, nullptr, |
| nullptr); |
| NavigateAndKeepLoading(web_contents(), url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.MatchHighConfidenceAllowlist.TriggerModel", 1); |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.PreClassificationCheckResult", |
| PreClassificationCheckResult::NO_CLASSIFY_MATCH_HC_ALLOWLIST, 1); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| TestPreClassificationCheckDoesNotMatchHighConfidenceAllowlist) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| std::vector<base::test::FeatureRef> enabled_features = {}; |
| enabled_features.push_back(kClientSideDetectionAcceptHCAllowlist); |
| SetFeatures(enabled_features, {}); |
| |
| csd_host_->set_high_confidence_allowlist_acceptance_rate_for_testing(0.0f); |
| base::HistogramTester histogram_tester; |
| |
| GURL url("http://host.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, /*match=*/true); |
| ExpectPreClassificationChecks(url, &kFalse, &kFalse, nullptr, nullptr, |
| nullptr); |
| NavigateAndKeepLoading(web_contents(), url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.MatchHighConfidenceAllowlist.TriggerModel", 1); |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.PreClassificationCheckResult", |
| PreClassificationCheckResult::NO_CLASSIFY_MATCH_HC_ALLOWLIST, 0); |
| } |
| |
| TEST_F( |
| ClientSideDetectionHostTest, |
| TestPreClassificationCheckDoesNotMatchHighConfidenceAllowlistDueToDisabledFeature) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| std::vector<base::test::FeatureRef> disabled_features = {}; |
| disabled_features.push_back(kClientSideDetectionAcceptHCAllowlist); |
| SetFeatures({}, disabled_features); |
| |
| // We will set the acceptance rate to 100%, but it won't be accepted because |
| // the feature is disabled. |
| csd_host_->set_high_confidence_allowlist_acceptance_rate_for_testing(1.0f); |
| base::HistogramTester histogram_tester; |
| |
| GURL url("http://host.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, /*match=*/true); |
| ExpectPreClassificationChecks(url, &kFalse, &kFalse, nullptr, nullptr, |
| nullptr); |
| NavigateAndKeepLoading(web_contents(), url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.MatchHighConfidenceAllowlist.TriggerModel", 1); |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.PreClassificationCheckResult", |
| PreClassificationCheckResult::NO_CLASSIFY_MATCH_HC_ALLOWLIST, 0); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| TestPreClassificationCheckSameDocumentNavigation) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| GURL url("http://host.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, false); |
| ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse, |
| &kFalse); |
| NavigateAndKeepLoading(web_contents(), url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| fake_phishing_detector_.CheckMessage(&url); |
| fake_phishing_detector_.Reset(); |
| |
| // Now try an same-document navigation. This should not trigger an IPC. |
| EXPECT_CALL(*csd_service_, IsPrivateIPAddress(_)).Times(0); |
| GURL inpage("http://host.com/#foo"); |
| ExpectPreClassificationChecks(inpage, nullptr, nullptr, nullptr, nullptr, |
| nullptr); |
| NavigateAndKeepLoading(web_contents(), inpage); |
| WaitAndCheckPreClassificationChecks(); |
| |
| fake_phishing_detector_.CheckMessage(nullptr); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, TestPreClassificationCheckXHTML) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| // Check that XHTML is supported, in addition to the default HTML type. |
| GURL url("http://host.com/xhtml"); |
| auto navigation = |
| content::NavigationSimulator::CreateBrowserInitiated(url, web_contents()); |
| navigation->SetContentsMimeType("application/xhtml+xml"); |
| navigation->SetKeepLoading(true); |
| |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, false); |
| |
| ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse, |
| &kFalse); |
| navigation->Commit(); |
| WaitAndCheckPreClassificationChecks(); |
| |
| fake_phishing_detector_.CheckMessage(&url); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, TestPreClassificationCheckTwoNavigations) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| // Navigate to two hosts, which should cause two IPCs. |
| GURL url1("http://host1.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url1, false); |
| ExpectPreClassificationChecks(url1, &kFalse, &kFalse, &kFalse, &kFalse, |
| &kFalse); |
| NavigateAndKeepLoading(web_contents(), url1); |
| WaitAndCheckPreClassificationChecks(); |
| |
| fake_phishing_detector_.CheckMessage(&url1); |
| |
| GURL url2("http://host2.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url2, false); |
| ExpectPreClassificationChecks(url2, &kFalse, &kFalse, &kFalse, &kFalse, |
| &kFalse); |
| NavigateAndKeepLoading(web_contents(), url2); |
| WaitAndCheckPreClassificationChecks(); |
| |
| fake_phishing_detector_.CheckMessage(&url2); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| TestPreClassificationCheckPrivateIpAddress) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| // If IsPrivateIPAddress returns true, no IPC should be triggered. |
| GURL url("http://host3.com/"); |
| ExpectPreClassificationChecks(url, &kTrue, nullptr, nullptr, nullptr, |
| nullptr); |
| NavigateAndCommit(url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| fake_phishing_detector_.CheckMessage(nullptr); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, TestPreClassificationCheckLocalResource) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| // If IsLocalResource returns true, no IPC should be triggered. |
| GURL url("http://host3.com/"); |
| ExpectPreClassificationChecks(url, nullptr, nullptr, nullptr, nullptr, |
| &kTrue); |
| NavigateAndCommit(url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| fake_phishing_detector_.CheckMessage(nullptr); |
| } |
| |
| TEST_F(ClientSideDetectionHostIncognitoTest, |
| TestPreClassificationCheckIncognito) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| // If the tab is incognito there should be no IPC. Also, we shouldn't |
| // even check the csd-allowlist. |
| GURL url("http://host4.com/"); |
| ExpectPreClassificationChecks(url, &kFalse, nullptr, nullptr, nullptr, |
| &kFalse); |
| |
| content::WebContentsTester::For(web_contents())->NavigateAndCommit(url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| fake_phishing_detector_.CheckMessage(nullptr); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| TestPreClassificationCheckOverPhishingReportingLimit) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| // If the url isn't in the cache and we are over the reporting limit, we |
| // don't do classification. |
| GURL url("http://host7.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, false); |
| ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kTrue, |
| &kFalse); |
| NavigateAndKeepLoading(web_contents(), url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| fake_phishing_detector_.CheckMessage(nullptr); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, TestPreClassificationCheckHttpsUrl) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| GURL url("https://host.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, false); |
| ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse, |
| &kFalse); |
| NavigateAndKeepLoading(web_contents(), url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| fake_phishing_detector_.CheckMessage(&url); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| TestPreClassificationCheckNoneHttpOrHttpsUrl) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| GURL url("file://host.com/"); |
| ExpectPreClassificationChecks(url, &kFalse, nullptr, nullptr, nullptr, |
| &kFalse); |
| NavigateAndKeepLoading(web_contents(), url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| fake_phishing_detector_.CheckMessage(nullptr); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, TestPreClassificationCheckValidCached) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| // If result is cached, we will try and display the blocking page directly |
| // with no start classification message. |
| GURL url("http://host8.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, false); |
| ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kTrue, &kFalse, |
| &kFalse); |
| |
| UnsafeResource resource; |
| EXPECT_CALL(*ui_manager_.get(), DisplayBlockingPage(_)) |
| .WillOnce(SaveArg<0>(&resource)); |
| |
| NavigateAndKeepLoading(web_contents(), url); |
| WaitAndCheckPreClassificationChecks(); |
| EXPECT_EQ(url, resource.url); |
| EXPECT_EQ(url, resource.original_url); |
| |
| fake_phishing_detector_.CheckMessage(nullptr); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, TestPreClassificationAllowlistedByPolicy) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| // Configures enterprise allowlist. |
| ScopedListPrefUpdate update(profile()->GetPrefs(), |
| prefs::kSafeBrowsingAllowlistDomains); |
| update->Append("example.com"); |
| |
| GURL url("http://example.com/"); |
| ExpectPreClassificationChecks(url, &kFalse, nullptr, nullptr, nullptr, |
| &kFalse); |
| |
| NavigateAndKeepLoading(web_contents(), url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| fake_phishing_detector_.CheckMessage(nullptr); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, RecordsPhishingDetectorResults) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| { |
| ClientPhishingRequest verdict; |
| verdict.set_url("http://not-phishing.com/"); |
| verdict.set_client_score(0.1f); |
| verdict.set_is_phishing(false); |
| |
| base::HistogramTester histogram_tester; |
| |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(_, _, _)) |
| .Times(0); |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| |
| histogram_tester.ExpectUniqueSample( |
| "SBClientPhishing.PhishingDetectorResult.TriggerModel", |
| mojom::PhishingDetectorResult::SUCCESS, 1); |
| } |
| |
| { |
| base::HistogramTester histogram_tester; |
| |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(_, _, _)) |
| .Times(0); |
| PhishingDetectionError(mojom::PhishingDetectorResult::CLASSIFIER_NOT_READY); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| |
| histogram_tester.ExpectUniqueSample( |
| "SBClientPhishing.PhishingDetectorResult.TriggerModel", |
| mojom::PhishingDetectorResult::CLASSIFIER_NOT_READY, 1); |
| } |
| |
| { |
| base::HistogramTester histogram_tester; |
| |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(_, _, _)) |
| .Times(0); |
| PhishingDetectionError( |
| mojom::PhishingDetectorResult::FORWARD_BACK_TRANSITION); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| |
| histogram_tester.ExpectUniqueSample( |
| "SBClientPhishing.PhishingDetectorResult.TriggerModel", |
| mojom::PhishingDetectorResult::FORWARD_BACK_TRANSITION, 1); |
| } |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, RecordsPhishingDetectionDuration) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| base::HistogramTester histogram_tester; |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.PhishingDetectionDuration.TriggerModel", 0); |
| |
| GURL start_url("http://safe.example.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(start_url, false); |
| ExpectPreClassificationChecks(start_url, &kFalse, &kFalse, &kFalse, &kFalse, |
| &kFalse); |
| NavigateAndCommit(start_url); |
| WaitAndCheckPreClassificationChecks(); |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.PhishingDetectionDuration.TriggerModel", 1); |
| |
| GURL url("http://phishing.example.com/"); |
| ClientPhishingRequest verdict; |
| verdict.set_url(url.spec()); |
| verdict.set_client_score(0.1f); |
| verdict.set_is_phishing(false); |
| |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, false); |
| ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse, |
| &kFalse); |
| NavigateAndCommit(url); |
| WaitAndCheckPreClassificationChecks(); |
| const base::TimeDelta duration = base::Milliseconds(10); |
| AdvanceTimeTickClock(duration); |
| |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.PhishingDetectionDuration.TriggerModel", 3); |
| EXPECT_LE(duration.InMilliseconds(), |
| histogram_tester |
| .GetAllSamples( |
| "SBClientPhishing.PhishingDetectionDuration.TriggerModel") |
| .front() |
| .min); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, PopulatesPageLoadToken) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| GURL url("http://phishing.example.com/"); |
| ClientPhishingRequest verdict; |
| verdict.set_client_score(1.0); |
| verdict.set_is_phishing(true); |
| |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, false); |
| ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse, |
| &kFalse); |
| NavigateAndCommit(url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| std::unique_ptr<ClientPhishingRequest> verdict_sent; |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(_, _, _)) |
| .WillOnce(MoveArg<0>(&verdict_sent)); |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| ASSERT_EQ(1, verdict_sent->population().page_load_tokens_size()); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| CSDFeaturesCacheContainsVerdictAndFullDebuggingMetadata) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| std::vector<base::test::FeatureRef> enabled_features = {}; |
| enabled_features.push_back(kClientSideDetectionDebuggingMetadataCache); |
| SetEnhancedProtectionPrefForTests(profile()->GetPrefs(), true); |
| SetFeatures(enabled_features, {}); |
| |
| ClientPhishingRequest* verdict_from_cache = nullptr; |
| LoginReputationClientRequest::DebuggingMetadata* debugging_metadata = nullptr; |
| |
| GURL example_url("http://phishingurl.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(example_url, false); |
| ExpectPreClassificationChecks( |
| /*url=*/example_url, /*is_private=*/&kFalse, |
| /*match_csd_allowlist=*/&kFalse, /*get_valid_cached_result=*/&kFalse, |
| /*over_phishing_report_limit=*/&kFalse, /*is_local=*/&kFalse); |
| NavigateAndCommit(example_url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| ClientSideDetectionService::ClientReportPhishingRequestCallback cb; |
| ClientPhishingRequest verdict; |
| verdict.set_url(example_url.spec()); |
| verdict.set_client_score(1.0f); |
| verdict.set_is_phishing(true); |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest( |
| PartiallyEqualVerdict(verdict), _, |
| "fake_access_token_for_debug_cache")) |
| .WillOnce(MoveArg<1>(&cb)); |
| |
| // Set up mock call to token fetcher. |
| SafeBrowsingTokenFetcher::Callback token_cb; |
| EXPECT_CALL(*raw_token_fetcher_, Start(_)) |
| .Times(1) |
| .WillRepeatedly(MoveArg<0>(&token_cb)); |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| |
| // Wait for token fetcher to be called. |
| EXPECT_TRUE(Mock::VerifyAndClear(raw_token_fetcher_)); |
| |
| ASSERT_FALSE(token_cb.is_null()); |
| std::move(token_cb).Run("fake_access_token_for_debug_cache"); |
| |
| // Token is now fetched, so we will now callback on |
| // ClientReportPhishingRequest. |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get())); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| |
| ASSERT_FALSE(cb.is_null()); |
| std::move(cb).Run(example_url, false, net::HTTP_OK, std::nullopt); |
| |
| ClientSideDetectionFeatureCache* feature_cache_map = |
| ClientSideDetectionFeatureCache::FromWebContents(web_contents()); |
| verdict_from_cache = feature_cache_map->GetVerdictForURL(example_url); |
| debugging_metadata = |
| feature_cache_map->GetDebuggingMetadataForURL(example_url); |
| |
| // Model version and force request field are not checked because the model |
| // isn't deployed, and the verdict cache manager is not populated. |
| EXPECT_NE(debugging_metadata, nullptr); |
| EXPECT_EQ(debugging_metadata->preclassification_check_result(), |
| PreClassificationCheckResult::CLASSIFY); |
| EXPECT_EQ(debugging_metadata->network_result(), net::HTTP_OK); |
| EXPECT_EQ(debugging_metadata->phishing_detector_result(), |
| PhishingDetectorResult::CLASSIFICATION_SUCCESS); |
| EXPECT_EQ(debugging_metadata->local_model_detects_phishing(), |
| verdict_from_cache->is_phishing()); |
| |
| EXPECT_NE(verdict_from_cache, nullptr); |
| EXPECT_EQ(verdict_from_cache->is_phishing(), verdict.is_phishing()); |
| EXPECT_EQ(verdict_from_cache->client_score(), verdict.client_score()); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| RTLookupResponseForceRequestSendsCSPPPingWhenVerdictNotPhishing) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| base::HistogramTester histogram_tester; |
| |
| SetEnhancedProtectionPrefForTests(profile()->GetPrefs(), true); |
| |
| SetFeatures({kClientSideDetectionSendLlamaForcedTriggerInfo}, {}); |
| GURL example_url("http://suspiciousurl.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(example_url, false); |
| ExpectPreClassificationChecks( |
| /*url=*/example_url, /*is_private=*/&kFalse, |
| /*match_csd_allowlist=*/&kFalse, /*get_valid_cached_result=*/&kFalse, |
| /*over_phishing_report_limit=*/&kFalse, /*is_local=*/&kFalse); |
| NavigateAndCommit(example_url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| VerdictCacheManager* cache_manager = |
| VerdictCacheManagerFactory::GetForProfile( |
| Profile::FromBrowserContext(web_contents()->GetBrowserContext())); |
| |
| RTLookupResponse response; |
| |
| RTLookupResponse::ThreatInfo* new_threat_info2 = response.add_threat_info(); |
| new_threat_info2->set_verdict_type(RTLookupResponse::ThreatInfo::DANGEROUS); |
| new_threat_info2->set_threat_type( |
| RTLookupResponse::ThreatInfo::SOCIAL_ENGINEERING); |
| new_threat_info2->set_cache_duration_sec(60); |
| new_threat_info2->set_cache_expression_using_match_type("suspiciousurl.com/"); |
| new_threat_info2->set_cache_expression_match_type( |
| RTLookupResponse::ThreatInfo::EXACT_MATCH); |
| |
| response.set_client_side_detection_type( |
| safe_browsing::ClientSideDetectionType::FORCE_REQUEST); |
| cache_manager->CacheRealTimeUrlVerdict(response, base::Time::Now()); |
| EXPECT_EQ( |
| static_cast<int>(safe_browsing::ClientSideDetectionType::FORCE_REQUEST), |
| cache_manager->GetCachedRealTimeUrlClientSideDetectionType(example_url)); |
| |
| ClientSideDetectionService::ClientReportPhishingRequestCallback cb; |
| |
| // The verdict's is_phishing is false, but we will still send a ping! |
| ClientPhishingRequest verdict; |
| verdict.set_url(example_url.spec()); |
| verdict.set_client_score(0.8f); |
| verdict.set_is_phishing(false); |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest( |
| PartiallyEqualVerdict(verdict), _, |
| "fake_access_token_for_force_request")) |
| .WillOnce(MoveArg<1>(&cb)); |
| |
| // Set up mock call to token fetcher. |
| SafeBrowsingTokenFetcher::Callback token_cb; |
| EXPECT_CALL(*raw_token_fetcher_, Start(_)) |
| .Times(1) |
| .WillRepeatedly(MoveArg<0>(&token_cb)); |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| |
| // Wait for token fetcher to be called. |
| EXPECT_TRUE(Mock::VerifyAndClear(raw_token_fetcher_)); |
| |
| ASSERT_FALSE(token_cb.is_null()); |
| std::move(token_cb).Run("fake_access_token_for_force_request"); |
| |
| // Token is now fetched, so we will now callback on |
| // ClientReportPhishingRequest. |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get())); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| |
| ASSERT_FALSE(cb.is_null()); |
| std::move(cb).Run(example_url, false, net::HTTP_OK, std::nullopt); |
| |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", |
| ClientSideDetectionType::FORCE_REQUEST, 1); |
| histogram_tester.ExpectBucketCount("SBClientPhishing.RTLookupForceRequest", |
| true, 1); |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.RTLookupForceRequest.HasLlamaForcedTriggerInfo", false, |
| 1); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| RTLookupResponseOnFirstURLInRedirectChainTriggersForceRequest) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| base::HistogramTester histogram_tester; |
| |
| SetEnhancedProtectionPrefForTests(profile()->GetPrefs(), true); |
| |
| GURL first_url_redirect("http://firsturlsuspicious.com/"); |
| GURL second_url_redirect("http://secondurlnotsuspicious.com/"); |
| GURL third_url_redirect("http://thirdurlnotsuspicious.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(first_url_redirect, false); |
| database_manager_->SetAllowlistLookupDetailsForUrl(second_url_redirect, |
| false); |
| database_manager_->SetAllowlistLookupDetailsForUrl(third_url_redirect, false); |
| |
| auto navigation = content::NavigationSimulator::CreateBrowserInitiated( |
| first_url_redirect, web_contents()); |
| navigation->Start(); |
| navigation->Redirect(second_url_redirect); |
| navigation->Redirect(third_url_redirect); |
| navigation->Commit(); |
| |
| content::NavigationEntry* entry = |
| web_contents()->GetController().GetVisibleEntry(); |
| |
| ASSERT_TRUE(entry); |
| EXPECT_EQ(entry->GetRedirectChain().size(), 3u); |
| |
| VerdictCacheManager* cache_manager = |
| VerdictCacheManagerFactory::GetForProfile( |
| Profile::FromBrowserContext(web_contents()->GetBrowserContext())); |
| |
| // We will only create a RTLookupResponse for the first URL and cache it in |
| // the cache_manager. |
| RTLookupResponse response; |
| |
| RTLookupResponse::ThreatInfo* new_threat_info2 = response.add_threat_info(); |
| new_threat_info2->set_verdict_type(RTLookupResponse::ThreatInfo::DANGEROUS); |
| new_threat_info2->set_threat_type( |
| RTLookupResponse::ThreatInfo::SOCIAL_ENGINEERING); |
| new_threat_info2->set_cache_duration_sec(60); |
| new_threat_info2->set_cache_expression_using_match_type( |
| "firsturlsuspicious.com/"); |
| new_threat_info2->set_cache_expression_match_type( |
| RTLookupResponse::ThreatInfo::EXACT_MATCH); |
| |
| response.set_client_side_detection_type( |
| safe_browsing::ClientSideDetectionType::FORCE_REQUEST); |
| cache_manager->CacheRealTimeUrlVerdict(response, base::Time::Now()); |
| EXPECT_EQ( |
| static_cast<int>(safe_browsing::ClientSideDetectionType::FORCE_REQUEST), |
| cache_manager->GetCachedRealTimeUrlClientSideDetectionType( |
| first_url_redirect)); |
| |
| ClientSideDetectionService::ClientReportPhishingRequestCallback cb; |
| |
| // The verdict's is_phishing is false, but we will still send a ping! We are |
| // using the third URL for the verdict because it's the last in the referrer |
| // chain, but the first is in the cache, so it should still send a ping. |
| ClientPhishingRequest verdict; |
| verdict.set_url(third_url_redirect.spec()); |
| verdict.set_client_score(0.8f); |
| verdict.set_is_phishing(false); |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest( |
| PartiallyEqualVerdict(verdict), _, |
| "fake_access_token_for_force_request")) |
| .WillOnce(MoveArg<1>(&cb)); |
| |
| // Set up mock call to token fetcher. |
| SafeBrowsingTokenFetcher::Callback token_cb; |
| EXPECT_CALL(*raw_token_fetcher_, Start(_)) |
| .Times(1) |
| .WillRepeatedly(MoveArg<0>(&token_cb)); |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| |
| // Wait for token fetcher to be called. |
| EXPECT_TRUE(Mock::VerifyAndClear(raw_token_fetcher_)); |
| |
| ASSERT_FALSE(token_cb.is_null()); |
| std::move(token_cb).Run("fake_access_token_for_force_request"); |
| |
| // Token is now fetched, so we will now callback on |
| // ClientReportPhishingRequest. |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get())); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| |
| ASSERT_FALSE(cb.is_null()); |
| std::move(cb).Run(third_url_redirect, false, net::HTTP_OK, std::nullopt); |
| |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", |
| ClientSideDetectionType::FORCE_REQUEST, 1); |
| histogram_tester.ExpectBucketCount("SBClientPhishing.RTLookupForceRequest", |
| true, 1); |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.RedirectChainContainsForceRequest", true, 1); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| NoRTLookupResponseInRedirectChainContainsForceRequest) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| base::HistogramTester histogram_tester; |
| |
| SetEnhancedProtectionPrefForTests(profile()->GetPrefs(), true); |
| |
| GURL first_url_redirect("http://firsturlnotsuspicious.com/"); |
| GURL second_url_redirect("http://secondurlnotsuspicious.com/"); |
| GURL third_url_redirect("http://thirdurlnotsuspicious.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(first_url_redirect, false); |
| database_manager_->SetAllowlistLookupDetailsForUrl(second_url_redirect, |
| false); |
| database_manager_->SetAllowlistLookupDetailsForUrl(third_url_redirect, false); |
| |
| auto navigation = content::NavigationSimulator::CreateBrowserInitiated( |
| first_url_redirect, web_contents()); |
| navigation->Start(); |
| navigation->Redirect(second_url_redirect); |
| navigation->Redirect(third_url_redirect); |
| navigation->Commit(); |
| |
| content::NavigationEntry* entry = |
| web_contents()->GetController().GetVisibleEntry(); |
| |
| ASSERT_TRUE(entry); |
| EXPECT_EQ(entry->GetRedirectChain().size(), 3u); |
| |
| // The verdict's is_phishing is false, and there are no force requests at all |
| // in the current URL and its redirect chain, so no ping will be sent. |
| ClientPhishingRequest verdict; |
| verdict.set_url(third_url_redirect.spec()); |
| verdict.set_client_score(0.8f); |
| verdict.set_is_phishing(false); |
| |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get())); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", |
| ClientSideDetectionType::FORCE_REQUEST, 0); |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", |
| ClientSideDetectionType::TRIGGER_MODELS, 1); |
| histogram_tester.ExpectBucketCount("SBClientPhishing.RTLookupForceRequest", |
| false, 1); |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.RedirectChainContainsForceRequest", false, 1); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| RedirectChainKillswitchDoesNotTriggersForceRequest) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionRedirectChainKillswitch}, {}); |
| |
| base::HistogramTester histogram_tester; |
| |
| SetEnhancedProtectionPrefForTests(profile()->GetPrefs(), true); |
| |
| GURL first_url_redirect("http://firsturlsuspicious.com/"); |
| GURL second_url_redirect("http://secondurlnotsuspicious.com/"); |
| GURL third_url_redirect("http://thirdurlnotsuspicious.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(first_url_redirect, false); |
| database_manager_->SetAllowlistLookupDetailsForUrl(second_url_redirect, |
| false); |
| database_manager_->SetAllowlistLookupDetailsForUrl(third_url_redirect, false); |
| |
| auto navigation = content::NavigationSimulator::CreateBrowserInitiated( |
| first_url_redirect, web_contents()); |
| navigation->Start(); |
| navigation->Redirect(second_url_redirect); |
| navigation->Redirect(third_url_redirect); |
| navigation->Commit(); |
| |
| content::NavigationEntry* entry = |
| web_contents()->GetController().GetVisibleEntry(); |
| |
| ASSERT_TRUE(entry); |
| EXPECT_EQ(entry->GetRedirectChain().size(), 3u); |
| |
| VerdictCacheManager* cache_manager = |
| VerdictCacheManagerFactory::GetForProfile( |
| Profile::FromBrowserContext(web_contents()->GetBrowserContext())); |
| |
| // We will only create a RTLookupResponse for the first URL and cache it in |
| // the cache_manager. |
| RTLookupResponse response; |
| |
| RTLookupResponse::ThreatInfo* new_threat_info2 = response.add_threat_info(); |
| new_threat_info2->set_verdict_type(RTLookupResponse::ThreatInfo::DANGEROUS); |
| new_threat_info2->set_threat_type( |
| RTLookupResponse::ThreatInfo::SOCIAL_ENGINEERING); |
| new_threat_info2->set_cache_duration_sec(60); |
| new_threat_info2->set_cache_expression_using_match_type( |
| "firsturlsuspicious.com/"); |
| new_threat_info2->set_cache_expression_match_type( |
| RTLookupResponse::ThreatInfo::EXACT_MATCH); |
| |
| response.set_client_side_detection_type( |
| safe_browsing::ClientSideDetectionType::FORCE_REQUEST); |
| cache_manager->CacheRealTimeUrlVerdict(response, base::Time::Now()); |
| EXPECT_EQ( |
| static_cast<int>(safe_browsing::ClientSideDetectionType::FORCE_REQUEST), |
| cache_manager->GetCachedRealTimeUrlClientSideDetectionType( |
| first_url_redirect)); |
| |
| // The verdict's is_phishing is false, but we will still send a ping! We are |
| // using the third URL for the verdict because it's the last in the referrer |
| // chain, but the first is in the cache, so it should still send a ping. |
| ClientPhishingRequest verdict; |
| verdict.set_url(third_url_redirect.spec()); |
| verdict.set_client_score(0.8f); |
| verdict.set_is_phishing(false); |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| |
| // Token is now fetched, so we will now callback on |
| // ClientReportPhishingRequest. |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get())); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", |
| ClientSideDetectionType::TRIGGER_MODELS, 1); |
| histogram_tester.ExpectBucketCount("SBClientPhishing.RTLookupForceRequest", |
| false, 1); |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.RedirectChainContainsForceRequest", 0); |
| } |
| |
| class ClientSideDetectionHostNotificationTest |
| : public ClientSideDetectionHostTest { |
| public: |
| ClientSideDetectionHostNotificationTest() = default; |
| |
| void SetUp() override { |
| ClientSideDetectionHostTest::SetUp(); |
| |
| SetEnhancedProtectionPrefForTests(profile()->GetPrefs(), true); |
| SetFeatures({kClientSideDetectionNotificationPrompt}, {}); |
| |
| permissions::PermissionRequestManager::CreateForWebContents(web_contents()); |
| auto* manager = |
| permissions::PermissionRequestManager::FromWebContents(web_contents()); |
| manager->set_enabled_app_level_notification_permission_for_testing(true); |
| prompt_factory_ = |
| std::make_unique<permissions::MockPermissionPromptFactory>(manager); |
| |
| // Set the prefs and feature and then register the request manager because |
| // this is set up on tab start, but the prefs were not set when the test is |
| // created. |
| csd_host_->RegisterPermissionRequestManager(); |
| manager->clear_permission_ui_selector_for_testing(); |
| } |
| |
| void TearDown() override { |
| prompt_factory_.reset(); |
| ClientSideDetectionHostTest::TearDown(); |
| } |
| |
| void PhishingDetectionDone(mojo_base::ProtoWrapper verdict) { |
| csd_host_->PhishingDetectionDone( |
| ClientSideDetectionType::NOTIFICATION_PERMISSION_PROMPT, |
| /*is_sample_ping=*/false, /*did_match_high_confidence_allowlist=*/false, |
| mojom::PhishingDetectorResult::SUCCESS, std::move(verdict)); |
| } |
| |
| void PhishingDetectionError(mojom::PhishingDetectorResult error) { |
| csd_host_->PhishingDetectionDone( |
| ClientSideDetectionType::NOTIFICATION_PERMISSION_PROMPT, |
| /*is_sample_ping=*/false, /*did_match_high_confidence_allowlist=*/false, |
| error, std::nullopt); |
| } |
| |
| void WaitForBubbleToBeShown() { |
| auto* manager = |
| permissions::PermissionRequestManager::FromWebContents(web_contents()); |
| manager->DocumentOnLoadCompletedInPrimaryMainFrame(); |
| task_environment()->RunUntilIdle(); |
| } |
| |
| protected: |
| std::unique_ptr<permissions::MockPermissionPromptFactory> prompt_factory_; |
| }; |
| |
| TEST_F(ClientSideDetectionHostNotificationTest, |
| NotificationPermissionPromptTriggersClassificationRequest) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| base::HistogramTester histogram_tester; |
| |
| // First navigate to a page, which should trigger preclassification check. |
| GURL url("http://example.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, false); |
| ExpectPreClassificationChecks( |
| url, /*is_private=*/&kFalse, /*match_csd_allowlist=*/&kFalse, |
| /*get_valid_cached_result=*/&kFalse, |
| /*over_phishing_report_limit=*/&kFalse, /*is_local=*/&kFalse); |
| NavigateAndCommit(url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", |
| ClientSideDetectionType::TRIGGER_MODELS, 1); |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", 1); |
| |
| // Second, create a permission request that's specifically notifications, and |
| // add to the request manager, which should also trigger a preclassification |
| // check, this will skip the expect call for GetValidCachedResult. In |
| // addition, we do not check the cache if the request type was not through |
| // trigger model. |
| ExpectPreClassificationChecks( |
| url, /*is_private=*/&kFalse, /*match_csd_allowlist=*/&kFalse, |
| /*get_valid_cached_result=*/nullptr, |
| /*over_phishing_report_limit=*/&kFalse, /*is_local=*/&kFalse); |
| |
| ClientPhishingRequest verdict; |
| verdict.set_client_score(0.8f); |
| |
| EXPECT_CALL(*csd_service_, |
| SendClientReportPhishingRequest( |
| PartiallyEqualVerdict(verdict), _, |
| "fake_access_token_notification_permission_prompt")); |
| |
| // Set up mock call to token fetcher. |
| SafeBrowsingTokenFetcher::Callback cb; |
| EXPECT_CALL(*raw_token_fetcher_, Start(_)).WillOnce(MoveArg<0>(&cb)); |
| permissions::MockPermissionRequest::MockPermissionRequestState request_state; |
| auto request1 = std::make_unique<permissions::MockPermissionRequest>( |
| url, permissions::RequestType::kNotifications, |
| permissions::PermissionRequestGestureType::GESTURE, |
| request_state.GetWeakPtr()); |
| auto* manager = |
| permissions::PermissionRequestManager::FromWebContents(web_contents()); |
| manager->AddRequest(web_contents()->GetPrimaryMainFrame(), |
| std::move(request1)); |
| |
| WaitForBubbleToBeShown(); |
| |
| EXPECT_TRUE(prompt_factory_->is_visible()); |
| EXPECT_TRUE(prompt_factory_->RequestTypeSeen(request_state.request_type)); |
| ASSERT_EQ(prompt_factory_->request_count(), 1); |
| |
| // Wait for token fetcher to be called. |
| EXPECT_TRUE(Mock::VerifyAndClear(raw_token_fetcher_)); |
| |
| ASSERT_FALSE(cb.is_null()); |
| std::move(cb).Run("fake_access_token_notification_permission_prompt"); |
| |
| manager->Accept(); |
| task_environment()->RunUntilIdle(); |
| |
| EXPECT_TRUE(request_state.granted); |
| |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.PhishingDetectorResult.NotificationPermissionPrompt", |
| 1); |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", |
| ClientSideDetectionType::NOTIFICATION_PERMISSION_PROMPT, 1); |
| // Below histogram checks that there has been two classification requests. |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", 2); |
| } |
| |
| TEST_F(ClientSideDetectionHostNotificationTest, |
| NotPhishingVerdictSendsPingFromNotificationPermissionPrompt) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| base::HistogramTester histogram_tester; |
| |
| ClientPhishingRequest verdict; |
| verdict.set_url("http://example.com/"); |
| verdict.set_client_score(0.8f); |
| verdict.set_is_phishing(false); |
| verdict.set_client_side_detection_type( |
| ClientSideDetectionType::NOTIFICATION_PERMISSION_PROMPT); |
| |
| EXPECT_CALL(*csd_service_, |
| SendClientReportPhishingRequest( |
| PartiallyEqualVerdict(verdict), _, |
| "fake_access_token_notification_permission_prompt")); |
| |
| // Set up mock call to token fetcher. |
| SafeBrowsingTokenFetcher::Callback cb; |
| EXPECT_CALL(*raw_token_fetcher_, Start(_)).WillOnce(MoveArg<0>(&cb)); |
| |
| // Make the call. |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| |
| // Wait for token fetcher to be called. |
| EXPECT_TRUE(Mock::VerifyAndClear(raw_token_fetcher_)); |
| |
| ASSERT_FALSE(cb.is_null()); |
| std::move(cb).Run("fake_access_token_notification_permission_prompt"); |
| |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", |
| ClientSideDetectionType::NOTIFICATION_PERMISSION_PROMPT, 1); |
| } |
| |
| class ClientSideDetectionRTLookupResponseForceRequestTest |
| : public ClientSideDetectionHostTest { |
| public: |
| ClientSideDetectionRTLookupResponseForceRequestTest() = default; |
| |
| void SetUp() override { |
| ClientSideDetectionHostTest::SetUp(); |
| |
| SetEnhancedProtectionPrefForTests(profile()->GetPrefs(), true); |
| SetFeatures({kClientSideDetectionAcceptHCAllowlist}, {}); |
| |
| AsyncCheckTracker::CreateForWebContents( |
| web_contents(), |
| /*ui_manager=*/nullptr, |
| /*should_sync_checker_check_allowlist=*/false); |
| csd_host_->RegisterAsyncCheckTracker(); |
| } |
| |
| protected: |
| void SetRTResponseInCacheManager(bool is_enforced) { |
| VerdictCacheManager* cache_manager = |
| VerdictCacheManagerFactory::GetForProfile( |
| Profile::FromBrowserContext(web_contents()->GetBrowserContext())); |
| RTLookupResponse response; |
| RTLookupResponse::ThreatInfo* new_threat_info = response.add_threat_info(); |
| new_threat_info->set_verdict_type(RTLookupResponse::ThreatInfo::DANGEROUS); |
| new_threat_info->set_threat_type( |
| RTLookupResponse::ThreatInfo::SOCIAL_ENGINEERING); |
| new_threat_info->set_cache_duration_sec(60); |
| new_threat_info->set_cache_expression_using_match_type( |
| "suspiciousurl.com/"); |
| new_threat_info->set_cache_expression_match_type( |
| RTLookupResponse::ThreatInfo::EXACT_MATCH); |
| |
| response.set_client_side_detection_type( |
| is_enforced ? safe_browsing::ClientSideDetectionType::FORCE_REQUEST |
| : safe_browsing::ClientSideDetectionType:: |
| CLIENT_SIDE_DETECTION_TYPE_UNSPECIFIED); |
| cache_manager->CacheRealTimeUrlVerdict(response, base::Time::Now()); |
| } |
| |
| void CompleteAsyncCheck() { |
| auto* tracker = AsyncCheckTracker::GetOrCreateForWebContents( |
| web_contents(), /*ui_manager=*/nullptr, |
| /*should_sync_checker_check_allowlist=*/false); |
| auto checker = std::make_unique<UrlCheckerHolder>( |
| /*delegate_getter=*/base::NullCallback(), content::FrameTreeNodeId(), |
| /*navigation_id=*/0, |
| /*web_contents_getter=*/base::NullCallback(), |
| /*complete_callback=*/base::NullCallback(), |
| /*url_real_time_lookup_enabled=*/false, |
| /*can_check_db=*/true, |
| /*can_check_high_confidence_allowlist=*/true, |
| /*url_lookup_service_metric_suffix=*/"", |
| /*url_lookup_service=*/nullptr, |
| /*hash_realtime_service=*/nullptr, |
| /*hash_realtime_selection=*/ |
| hash_realtime_utils::HashRealTimeSelection::kNone, |
| /*is_async_check=*/true, /*check_allowlist_before_hash_database=*/false, |
| SessionID::InvalidValue(), /*referring_app_info=*/std::nullopt); |
| tracker->TransferUrlChecker(std::move(checker)); |
| // all_checks_completed must be set to true to notify |
| // ClientSideDetectionHost. |
| UrlCheckerHolder::OnCompleteCheckResult result( |
| /*proceed=*/true, /*showed_interstitial=*/false, |
| /*has_post_commit_interstitial_skipped=*/false, |
| SafeBrowsingUrlCheckerImpl::PerformedCheck::kUrlRealTimeCheck, |
| /*all_checks_completed=*/true); |
| tracker->PendingCheckerCompleted(/*navigation_id=*/0, result); |
| } |
| }; |
| |
| TEST_F(ClientSideDetectionRTLookupResponseForceRequestTest, |
| AsyncCheckTrackerTriggersClassificationRequest) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| base::HistogramTester histogram_tester; |
| |
| GURL example_url("http://suspiciousurl.com/"); |
| |
| database_manager_->SetAllowlistLookupDetailsForUrl(example_url, false); |
| |
| // First navigate to a page, which should trigger preclassification check. |
| ExpectPreClassificationChecks( |
| /*url=*/example_url, /*is_private=*/&kFalse, |
| /*match_csd_allowlist=*/&kFalse, /*get_valid_cached_result=*/&kFalse, |
| /*over_phishing_report_limit=*/&kFalse, /*is_local=*/&kFalse); |
| NavigateAndCommit(example_url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| // Force request should not be triggered, because RTLookupResponse hasn't |
| // been cached. |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", |
| ClientSideDetectionType::FORCE_REQUEST, 0); |
| |
| SetRTResponseInCacheManager(/*is_enforced=*/true); |
| |
| // get_valid_cached_result is set to nullptr, because the request type is not |
| // TRIGGER_MODELS. Force request triggers also bypass the CSD allowlist. |
| ExpectPreClassificationChecks( |
| example_url, &kFalse, /*match_csd_allowlist=*/nullptr, |
| /*get_valid_cached_result=*/nullptr, &kFalse, &kFalse); |
| // This call should trigger preclassification check again. |
| CompleteAsyncCheck(); |
| |
| // Set up mock call to token fetcher. |
| SafeBrowsingTokenFetcher::Callback token_cb; |
| EXPECT_CALL(*raw_token_fetcher_, Start(_)) |
| .Times(1) |
| .WillRepeatedly(MoveArg<0>(&token_cb)); |
| task_environment()->RunUntilIdle(); |
| |
| ClientSideDetectionService::ClientReportPhishingRequestCallback cb; |
| // The verdict's is_phishing is false, but we will still send a ping! |
| ClientPhishingRequest verdict; |
| verdict.set_url(example_url.spec()); |
| verdict.set_client_score(0.8f); |
| verdict.set_is_phishing(false); |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest( |
| _, _, "fake_access_token_for_force_request")) |
| .WillOnce(MoveArg<1>(&cb)); |
| |
| ASSERT_FALSE(token_cb.is_null()); |
| std::move(token_cb).Run("fake_access_token_for_force_request"); |
| task_environment()->RunUntilIdle(); |
| |
| // Token is now fetched, so we will now callback on |
| // ClientReportPhishingRequest. |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| |
| task_environment()->RunUntilIdle(); |
| |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", |
| ClientSideDetectionType::FORCE_REQUEST, 1); |
| histogram_tester.ExpectUniqueSample( |
| "SBClientPhishing.ClientSideDetection." |
| "AsyncCheckTriggerForceRequestResult", |
| ClientSideDetectionHost::AsyncCheckTriggerForceRequestResult::kTriggered, |
| 1); |
| } |
| |
| TEST_F(ClientSideDetectionRTLookupResponseForceRequestTest, |
| AsyncCheckTrackerTriggersClassificationRequestOnAllowlistMatch) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| base::HistogramTester histogram_tester; |
| |
| GURL example_url("http://suspiciousurl.com/"); |
| |
| database_manager_->SetAllowlistLookupDetailsForUrl(example_url, false); |
| |
| // First navigate to a page, which should trigger preclassification check. |
| ExpectPreClassificationChecks( |
| /*url=*/example_url, /*is_private=*/&kFalse, |
| /*match_csd_allowlist=*/&kFalse, /*get_valid_cached_result=*/&kFalse, |
| /*over_phishing_report_limit=*/&kFalse, /*is_local=*/&kFalse); |
| NavigateAndCommit(example_url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| // Force request should not be triggered, because RTLookupResponse hasn't |
| // been cached. |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", |
| ClientSideDetectionType::FORCE_REQUEST, 0); |
| |
| SetRTResponseInCacheManager(/*is_enforced=*/true); |
| |
| // Generally, this never happens unless a sampled RTLookupResponse contains |
| // the suspicious URL. For the purpose of this test, we will set that there's |
| // a match. |
| csd_host_->set_high_confidence_allowlist_acceptance_rate_for_testing(1.0f); |
| database_manager_->SetAllowlistLookupDetailsForUrl(example_url, true); |
| |
| // get_valid_cached_result is set to nullptr, because the request type is not |
| // TRIGGER_MODELS. Force request bypasses CSD allowlist check. |
| ExpectPreClassificationChecks( |
| example_url, &kFalse, /*match_csd_allowlist=*/nullptr, |
| /*get_valid_cached_result=*/nullptr, &kFalse, &kFalse); |
| // This call should trigger preclassification check again. |
| CompleteAsyncCheck(); |
| |
| // Set up mock call to token fetcher. |
| SafeBrowsingTokenFetcher::Callback token_cb; |
| EXPECT_CALL(*raw_token_fetcher_, Start(_)) |
| .Times(1) |
| .WillRepeatedly(MoveArg<0>(&token_cb)); |
| task_environment()->RunUntilIdle(); |
| |
| ClientSideDetectionService::ClientReportPhishingRequestCallback cb; |
| // The verdict's is_phishing is false, but we will still send a ping! |
| ClientPhishingRequest verdict; |
| verdict.set_url(example_url.spec()); |
| verdict.set_client_score(0.8f); |
| verdict.set_is_phishing(false); |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest( |
| _, _, "fake_access_token_for_force_request")) |
| .WillOnce(MoveArg<1>(&cb)); |
| |
| ASSERT_FALSE(token_cb.is_null()); |
| std::move(token_cb).Run("fake_access_token_for_force_request"); |
| task_environment()->RunUntilIdle(); |
| |
| // Token is now fetched, so we will now callback on |
| // ClientReportPhishingRequest. |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| |
| task_environment()->RunUntilIdle(); |
| |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", |
| ClientSideDetectionType::FORCE_REQUEST, 1); |
| histogram_tester.ExpectUniqueSample( |
| "SBClientPhishing.ClientSideDetection." |
| "AsyncCheckTriggerForceRequestResult", |
| ClientSideDetectionHost::AsyncCheckTriggerForceRequestResult::kTriggered, |
| 1); |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.MatchHighConfidenceAllowlist.ForceRequest", 1); |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.PreClassificationCheckResult", |
| PreClassificationCheckResult::NO_CLASSIFY_MATCH_HC_ALLOWLIST, 0); |
| } |
| |
| TEST_F(ClientSideDetectionRTLookupResponseForceRequestTest, |
| AsyncCheckTrackerNotTriggerClassificationRequestNoEnforcedPing) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| base::HistogramTester histogram_tester; |
| |
| GURL example_url("http://suspiciousurl.com/"); |
| |
| database_manager_->SetAllowlistLookupDetailsForUrl(example_url, false); |
| |
| // First navigate to a page, which should trigger preclassification check. |
| ExpectPreClassificationChecks( |
| /*url=*/example_url, /*is_private=*/&kFalse, |
| /*match_csd_allowlist=*/&kFalse, /*get_valid_cached_result=*/&kFalse, |
| /*over_phishing_report_limit=*/&kFalse, /*is_local=*/&kFalse); |
| NavigateAndCommit(example_url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| SetRTResponseInCacheManager(/*is_enforced=*/false); |
| |
| CompleteAsyncCheck(); |
| task_environment()->RunUntilIdle(); |
| |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", |
| ClientSideDetectionType::FORCE_REQUEST, 0); |
| histogram_tester.ExpectUniqueSample( |
| "SBClientPhishing.ClientSideDetection." |
| "AsyncCheckTriggerForceRequestResult", |
| ClientSideDetectionHost::AsyncCheckTriggerForceRequestResult:: |
| kSkippedNotForced, |
| 1); |
| } |
| |
| TEST_F(ClientSideDetectionRTLookupResponseForceRequestTest, |
| AsyncCheckTrackerNotTriggerClassificationRequestAlreadyPhishing) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| base::HistogramTester histogram_tester; |
| |
| GURL example_url("http://suspiciousurl.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(example_url, false); |
| ExpectPreClassificationChecks( |
| /*url=*/example_url, /*is_private=*/&kFalse, |
| /*match_csd_allowlist=*/&kFalse, /*get_valid_cached_result=*/&kFalse, |
| /*over_phishing_report_limit=*/&kFalse, /*is_local=*/&kFalse); |
| NavigateAndCommit(example_url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| SafeBrowsingTokenFetcher::Callback token_cb; |
| EXPECT_CALL(*raw_token_fetcher_, Start(_)) |
| .Times(1) |
| .WillRepeatedly(MoveArg<0>(&token_cb)); |
| ClientSideDetectionService::ClientReportPhishingRequestCallback cb; |
| ClientPhishingRequest verdict; |
| verdict.set_url(example_url.spec()); |
| verdict.set_client_score(1.0f); |
| verdict.set_is_phishing(true); |
| PhishingDetectionDone(mojo_base::ProtoWrapper(verdict)); |
| task_environment()->RunUntilIdle(); |
| |
| // Check that the page is phishing and triggers a ping. |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.LocalModelDetectsPhishing", true, 1); |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.LocalModelDetectsPhishing.TriggerModel", true, 1); |
| |
| SetRTResponseInCacheManager(/*is_enforced=*/true); |
| |
| CompleteAsyncCheck(); |
| task_environment()->RunUntilIdle(); |
| |
| // Enforce request should not be triggered again, because the page is already |
| // phishing and the ping is already sent. |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", |
| ClientSideDetectionType::FORCE_REQUEST, 0); |
| histogram_tester.ExpectUniqueSample( |
| "SBClientPhishing.ClientSideDetection." |
| "AsyncCheckTriggerForceRequestResult", |
| ClientSideDetectionHost::AsyncCheckTriggerForceRequestResult:: |
| kSkippedTriggerModelsPingNotSkipped, |
| 1); |
| } |
| |
| class ClientSideDetectionHostDebugFeaturesTest |
| : public ClientSideDetectionHostTest { |
| public: |
| ClientSideDetectionHostDebugFeaturesTest() = default; |
| |
| void SetUp() override { |
| ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| command_line_.GetProcessCommandLine()->AppendSwitchPath( |
| "--csd-debug-feature-directory", temp_dir_.GetPath()); |
| |
| ClientSideDetectionHostTest::SetUp(); |
| } |
| |
| private: |
| base::ScopedTempDir temp_dir_; |
| base::test::ScopedCommandLine command_line_; |
| }; |
| |
| TEST_F(ClientSideDetectionHostDebugFeaturesTest, |
| SkipsAllowlistWhenDumpingFeatures) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| GURL url("http://host.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, false); |
| ExpectPreClassificationChecks(url, &kFalse, nullptr, nullptr, nullptr, |
| &kFalse); |
| EXPECT_CALL(*database_manager_.get(), CheckCsdAllowlistUrl(url, _)).Times(0); |
| NavigateAndKeepLoading(web_contents(), url); |
| WaitAndCheckPreClassificationChecks(); |
| fake_phishing_detector_.CheckMessage(&url); |
| } |
| |
| TEST_F(ClientSideDetectionHostDebugFeaturesTest, |
| SkipsCacheWhenDumpingFeatures) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| GURL url("http://host.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, false); |
| ExpectPreClassificationChecks(url, &kFalse, nullptr, nullptr, nullptr, |
| &kFalse); |
| EXPECT_CALL(*csd_service_, GetValidCachedResult(url, NotNull())).Times(0); |
| NavigateAndKeepLoading(web_contents(), url); |
| WaitAndCheckPreClassificationChecks(); |
| fake_phishing_detector_.CheckMessage(&url); |
| } |
| |
| TEST_F(ClientSideDetectionHostDebugFeaturesTest, |
| SkipsReportLimitWhenDumpingFeatures) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) |
| GTEST_SKIP(); |
| |
| GURL url("http://host.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, false); |
| ExpectPreClassificationChecks(url, &kFalse, nullptr, nullptr, nullptr, |
| &kFalse); |
| EXPECT_CALL(*csd_service_, AtPhishingReportLimit()).Times(0); |
| NavigateAndKeepLoading(web_contents(), url); |
| WaitAndCheckPreClassificationChecks(); |
| fake_phishing_detector_.CheckMessage(&url); |
| } |
| |
| class ClientSideDetectionHostScamDetectionTest |
| : public ClientSideDetectionHostTest { |
| public: |
| ClientSideDetectionHostScamDetectionTest() = default; |
| |
| void SetUp() override { |
| ClientSideDetectionHostTest::SetUp(); |
| SetEnhancedProtectionPrefForTests(profile()->GetPrefs(), true); |
| csd_service_->SetOnDeviceAvailabilityForTesting(true); |
| database_manager_->SetAllowlistLookupDetailsForUrl(example_url_, false); |
| |
| ON_CALL(*raw_token_fetcher_, Start(_)) |
| .WillByDefault( |
| testing::Invoke([&](SafeBrowsingTokenFetcher::Callback callback) { |
| std::move(callback).Run("fake_access_token"); |
| })); |
| NavigateAndCommit(example_url_); |
| } |
| |
| void CacheForcedTriggerInfo(bool has_llama_forced_trigger_info, |
| bool intelligent_scan, |
| const std::string& cache_expression) { |
| VerdictCacheManager* cache_manager = |
| VerdictCacheManagerFactory::GetForProfile( |
| Profile::FromBrowserContext(web_contents()->GetBrowserContext())); |
| |
| RTLookupResponse response; |
| |
| RTLookupResponse::ThreatInfo* new_threat_info2 = response.add_threat_info(); |
| new_threat_info2->set_verdict_type(RTLookupResponse::ThreatInfo::DANGEROUS); |
| new_threat_info2->set_threat_type( |
| RTLookupResponse::ThreatInfo::SOCIAL_ENGINEERING); |
| new_threat_info2->set_cache_duration_sec(60); |
| new_threat_info2->set_cache_expression_using_match_type(cache_expression); |
| new_threat_info2->set_cache_expression_match_type( |
| RTLookupResponse::ThreatInfo::EXACT_MATCH); |
| |
| response.set_client_side_detection_type( |
| safe_browsing::ClientSideDetectionType::FORCE_REQUEST); |
| |
| if (has_llama_forced_trigger_info) { |
| safe_browsing::LlamaForcedTriggerInfo llama_forced_trigger_info; |
| safe_browsing::LlamaTriggerRuleInfo* llama_trigger_rule_info = |
| llama_forced_trigger_info.add_llama_trigger_rule_infos(); |
| llama_trigger_rule_info->set_llama_trigger_rule_id(28); |
| llama_trigger_rule_info->set_intelligent_scan(intelligent_scan); |
| llama_forced_trigger_info.set_trigger_url(cache_expression); |
| llama_forced_trigger_info.set_intelligent_scan(intelligent_scan); |
| response.mutable_llama_forced_trigger_info()->Swap( |
| &llama_forced_trigger_info); |
| } |
| |
| cache_manager->CacheRealTimeUrlVerdict(response, base::Time::Now()); |
| EXPECT_EQ( |
| static_cast<int>(safe_browsing::ClientSideDetectionType::FORCE_REQUEST), |
| cache_manager->GetCachedRealTimeUrlClientSideDetectionType( |
| example_url_)); |
| if (has_llama_forced_trigger_info) { |
| safe_browsing::LlamaForcedTriggerInfo cache_llama_forced_trigger_info; |
| EXPECT_TRUE(cache_manager->GetCachedRealTimeLlamaForcedTriggerInfo( |
| example_url_, &cache_llama_forced_trigger_info)); |
| // Because llama_forced_trigger_info is not copied, but rather passed by |
| // reference, we explicitly check with direct values set to the object |
| // above. |
| EXPECT_EQ(cache_llama_forced_trigger_info.intelligent_scan(), |
| intelligent_scan); |
| EXPECT_EQ(static_cast<int>( |
| cache_llama_forced_trigger_info.llama_trigger_rule_infos() |
| .size()), |
| 1); |
| EXPECT_EQ(cache_llama_forced_trigger_info.llama_trigger_rule_infos() |
| .at(0) |
| .llama_trigger_rule_id(), |
| 28); |
| EXPECT_EQ(cache_llama_forced_trigger_info.llama_trigger_rule_infos() |
| .at(0) |
| .intelligent_scan(), |
| intelligent_scan); |
| } |
| } |
| |
| void SetInquireOnDeviceModelCallback(bool should_return_response) { |
| EXPECT_CALL(*csd_service_, InquireOnDeviceModel(_, _)) |
| .WillOnce(testing::Invoke( |
| [=, this]( |
| std::string rendered_text, |
| base::OnceCallback<void( |
| std::optional< |
| optimization_guide::proto::ScamDetectionResponse>)> |
| callback) { |
| if (!should_return_response) { |
| std::move(callback).Run(std::nullopt); |
| return; |
| } |
| optimization_guide::proto::ScamDetectionResponse |
| scam_detection_response; |
| scam_detection_response.set_brand(example_brand_); |
| scam_detection_response.set_intent(example_intent_); |
| std::move(callback).Run(scam_detection_response); |
| })); |
| } |
| |
| void SetSendClientReportPhishingRequestCallback( |
| bool has_expected_brand_and_intent, |
| std::optional<IntelligentScanInfo::NoInfoReason> expected_no_info_reason, |
| std::optional<std::string> expected_llama_forced_trigger_info_trigger_url, |
| bool returned_is_phishing, |
| IntelligentScanVerdict returned_intelligent_scan_verdict) { |
| EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(_, _, _)) |
| .Times(1) |
| .WillOnce(testing::Invoke( |
| [=, this]( |
| std::unique_ptr<ClientPhishingRequest> request, |
| ClientSideDetectionService::ClientReportPhishingRequestCallback |
| callback, |
| const std::string&) { |
| if (has_expected_brand_and_intent) { |
| EXPECT_EQ(request->intelligent_scan_info().brand(), |
| example_brand_); |
| EXPECT_EQ(request->intelligent_scan_info().intent(), |
| example_intent_); |
| } else { |
| EXPECT_FALSE(request->intelligent_scan_info().has_brand()); |
| EXPECT_FALSE(request->intelligent_scan_info().has_intent()); |
| } |
| if (expected_no_info_reason.has_value()) { |
| EXPECT_EQ(request->intelligent_scan_info().no_info_reason(), |
| expected_no_info_reason.value()); |
| } else { |
| EXPECT_FALSE( |
| request->intelligent_scan_info().has_no_info_reason()); |
| } |
| if (expected_llama_forced_trigger_info_trigger_url.has_value()) { |
| EXPECT_EQ( |
| request->llama_forced_trigger_info().trigger_url(), |
| expected_llama_forced_trigger_info_trigger_url.value()); |
| } else { |
| EXPECT_FALSE( |
| request->llama_forced_trigger_info().has_trigger_url()); |
| } |
| std::move(callback).Run(example_url_, returned_is_phishing, |
| net::HTTP_OK, |
| returned_intelligent_scan_verdict); |
| })); |
| } |
| |
| void VerifyExpectedCalls() { |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get())); |
| EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); |
| EXPECT_TRUE(Mock::VerifyAndClear(raw_token_fetcher_)); |
| } |
| |
| void VerifyGeneralScamDetectionHistograms( |
| ClientSideDetectionType expected_request_type, |
| std::optional<bool> is_on_device_model_available, |
| std::optional<bool> model_has_successful_response, |
| std::optional<IntelligentScanVerdict> intelligent_scan_verdict) { |
| histogram_tester_.ExpectBucketCount( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", |
| expected_request_type, 1); |
| if (is_on_device_model_available) { |
| histogram_tester_.ExpectUniqueSample( |
| "SBClientPhishing.IsOnDeviceModelAvailableAtInquiryTime", |
| is_on_device_model_available.value(), 1); |
| histogram_tester_.ExpectUniqueSample( |
| "SBClientPhishing.IsOnDeviceModelAvailableAtInquiryTime." + |
| GetRequestTypeName(expected_request_type), |
| is_on_device_model_available.value(), 1); |
| } else { |
| histogram_tester_.ExpectTotalCount( |
| "SBClientPhishing.IsOnDeviceModelAvailableAtInquiryTime", 0); |
| histogram_tester_.ExpectTotalCount( |
| "SBClientPhishing.IsOnDeviceModelAvailableAtInquiryTime." + |
| GetRequestTypeName(expected_request_type), |
| 0); |
| } |
| if (model_has_successful_response.has_value()) { |
| histogram_tester_.ExpectUniqueSample( |
| "SBClientPhishing.OnDeviceModelHasSuccessfulResponse", |
| model_has_successful_response.value(), 1); |
| histogram_tester_.ExpectUniqueSample( |
| "SBClientPhishing.OnDeviceModelHasSuccessfulResponse." + |
| GetRequestTypeName(expected_request_type), |
| model_has_successful_response.value(), 1); |
| } else { |
| histogram_tester_.ExpectTotalCount( |
| "SBClientPhishing.OnDeviceModelHasSuccessfulResponse", 0); |
| histogram_tester_.ExpectTotalCount( |
| "SBClientPhishing.OnDeviceModelHasSuccessfulResponse." + |
| GetRequestTypeName(expected_request_type), |
| 0); |
| } |
| if (intelligent_scan_verdict.has_value()) { |
| histogram_tester_.ExpectUniqueSample( |
| "SBClientPhishing.IntelligentScanVerdict", |
| intelligent_scan_verdict.value(), 1); |
| } else { |
| histogram_tester_.ExpectTotalCount( |
| "SBClientPhishing.IntelligentScanVerdict", 0); |
| } |
| } |
| |
| void VerifyForcedTriggerScamDetectionHistograms( |
| bool force_request, |
| bool has_llama_forced_trigger_info, |
| bool intelligent_scan, |
| std::optional<bool> redirect_chain_contains_llama_forced_trigger_info) { |
| histogram_tester_.ExpectBucketCount("SBClientPhishing.RTLookupForceRequest", |
| force_request, 1); |
| histogram_tester_.ExpectBucketCount( |
| "SBClientPhishing.RTLookupForceRequest.HasLlamaForcedTriggerInfo", |
| has_llama_forced_trigger_info, 1); |
| if (redirect_chain_contains_llama_forced_trigger_info.has_value()) { |
| histogram_tester_.ExpectBucketCount( |
| "SBClientPhishing.RedirectChainContainsForcedTriggerInfo", |
| *redirect_chain_contains_llama_forced_trigger_info, 1); |
| } |
| |
| if (has_llama_forced_trigger_info) { |
| histogram_tester_.ExpectBucketCount( |
| "SBClientPhishing.LlamaForcedTriggerInfo.IntelligentScan", |
| intelligent_scan, 1); |
| histogram_tester_.ExpectBucketCount( |
| "SBClientPhishing.LlamaForcedTriggerInfo.LlamaTriggerRuleInfosSize", |
| 1, 1); |
| histogram_tester_.ExpectBucketCount( |
| "SBClientPhishing.LlamaForcedTriggerInfo.LlamaTriggerRuleId", 28, 1); |
| } |
| } |
| |
| void PhishingDetectionDone(bool is_phishing, |
| float client_score, |
| ClientSideDetectionType type, |
| bool did_match_high_confidence_allowlist) { |
| ClientPhishingRequest verdict; |
| verdict.set_url(example_url_.spec()); |
| verdict.set_client_score(client_score); |
| verdict.set_is_phishing(is_phishing); |
| csd_host_->PhishingDetectionDone(type, |
| /*is_sample_ping=*/false, |
| did_match_high_confidence_allowlist, |
| mojom::PhishingDetectorResult::SUCCESS, |
| mojo_base::ProtoWrapper(verdict)); |
| } |
| |
| void SetExampleUrl(GURL example_url) { example_url_ = example_url; } |
| |
| base::HistogramTester histogram_tester_; |
| GURL example_url_{"http://suspiciousurl.com/"}; |
| std::string example_brand_ = "Example Brand"; |
| std::string example_intent_ = "Example Intent"; |
| }; |
| |
| TEST_F(ClientSideDetectionHostScamDetectionTest, |
| KeyboardLockRequestTriggersOnDeviceLLMWithEmptyResponse) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionBrandAndIntentForScamDetection}, {}); |
| // Because the client side detection type is KEYBOARD_LOCK_REQUESTED, we will |
| // call to inquire the on-device model. |
| SetInquireOnDeviceModelCallback(/*should_return_response=*/false); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/false, |
| /*expected_no_info_reason=*/std::nullopt, |
| /*expected_llama_forced_trigger_info_trigger_url=*/std::nullopt, |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| |
| // Although the score is not phishy at all, we will still inquire the |
| // on-device model because the ping is triggered by keyboard lock. |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.0f, |
| ClientSideDetectionType::KEYBOARD_LOCK_REQUESTED, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType:: |
| KEYBOARD_LOCK_REQUESTED, |
| /*is_on_device_model_available=*/true, |
| /*model_has_successful_response=*/false, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| } |
| |
| TEST_F(ClientSideDetectionHostScamDetectionTest, |
| KeyboardLockRequestTriggersOnDeviceLLMWithFullResponse) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionBrandAndIntentForScamDetection}, {}); |
| SetInquireOnDeviceModelCallback(/*should_return_response=*/true); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/true, |
| /*expected_no_info_reason=*/std::nullopt, |
| /*expected_llama_forced_trigger_info_trigger_url=*/std::nullopt, |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.0f, |
| ClientSideDetectionType::KEYBOARD_LOCK_REQUESTED, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType:: |
| KEYBOARD_LOCK_REQUESTED, |
| /*is_on_device_model_available=*/true, |
| /*model_has_successful_response=*/true, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| } |
| |
| TEST_F(ClientSideDetectionHostScamDetectionTest, |
| EmptyInnerTextDoesNotTriggersOnDeviceLLM) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionBrandAndIntentForScamDetection}, {}); |
| raw_delegate_->ForceEmptyInnerText(); |
| // Because the inner text is empty, we will NOT inquire the on-device model. |
| EXPECT_CALL(*csd_service_, LogOnDeviceModelEligibilityReason()).Times(0); |
| EXPECT_CALL(*csd_service_, InquireOnDeviceModel(_, _)).Times(0); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/false, |
| /*expected_no_info_reason=*/IntelligentScanInfo::EMPTY_TEXT, |
| /*expected_llama_forced_trigger_info_trigger_url=*/std::nullopt, |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.0f, |
| ClientSideDetectionType::KEYBOARD_LOCK_REQUESTED, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType:: |
| KEYBOARD_LOCK_REQUESTED, |
| /*is_on_device_model_available=*/true, |
| /*model_has_successful_response=*/std::nullopt, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| } |
| |
| TEST_F(ClientSideDetectionHostScamDetectionTest, |
| AllowlistedOnHCDoesNotTriggersOnDeviceLLM) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionBrandAndIntentForScamDetection}, {}); |
| // Because the URL is on the HC allowlist, we will NOT inquire the |
| // on-device model. |
| EXPECT_CALL(*csd_service_, LogOnDeviceModelEligibilityReason()).Times(0); |
| EXPECT_CALL(*csd_service_, InquireOnDeviceModel(_, _)).Times(0); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/false, |
| /*expected_no_info_reason=*/IntelligentScanInfo::ALLOWLISTED, |
| /*expected_llama_forced_trigger_info_trigger_url=*/std::nullopt, |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.0f, |
| ClientSideDetectionType::KEYBOARD_LOCK_REQUESTED, |
| /*did_match_high_confidence_allowlist=*/true); |
| |
| VerifyExpectedCalls(); |
| // Allowlisted page does not check whether the on-device model is available, |
| // because it exists through the allowlist check beforehand. |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType:: |
| KEYBOARD_LOCK_REQUESTED, |
| /*is_on_device_model_available=*/std::nullopt, |
| /*model_has_successful_response=*/std::nullopt, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| } |
| |
| TEST_F(ClientSideDetectionHostScamDetectionTest, |
| NoOnDeviceModelDoesNotTriggersOnDeviceLLM) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionBrandAndIntentForScamDetection}, {}); |
| csd_service_->SetOnDeviceAvailabilityForTesting(false); |
| // Because the on-device model is unavailable, we will NOT inquire the |
| // on-device model. |
| EXPECT_CALL(*csd_service_, LogOnDeviceModelEligibilityReason()).Times(1); |
| EXPECT_CALL(*csd_service_, InquireOnDeviceModel(_, _)).Times(0); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/false, |
| /*expected_no_info_reason=*/ |
| IntelligentScanInfo::ON_DEVICE_MODEL_UNAVAILABLE, |
| /*expected_llama_forced_trigger_info_trigger_url=*/std::nullopt, |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.0f, |
| ClientSideDetectionType::KEYBOARD_LOCK_REQUESTED, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType:: |
| KEYBOARD_LOCK_REQUESTED, |
| /*is_on_device_model_available=*/false, |
| /*model_has_successful_response=*/std::nullopt, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| } |
| |
| TEST_F(ClientSideDetectionHostScamDetectionTest, |
| NonKeyboardLockRequestDoesNotTriggersOnDeviceLLM) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionBrandAndIntentForScamDetection}, {}); |
| |
| // Because the client side detection type is POINTER_LOCK_REQUESTED, we will |
| // NOT inquire the on-device model. |
| EXPECT_CALL(*csd_service_, LogOnDeviceModelEligibilityReason()).Times(0); |
| EXPECT_CALL(*csd_service_, InquireOnDeviceModel(_, _)).Times(0); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/false, |
| /*expected_no_info_reason=*/std::nullopt, |
| /*expected_llama_forced_trigger_info_trigger_url=*/std::nullopt, |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.0f, |
| ClientSideDetectionType::POINTER_LOCK_REQUESTED, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| // Because the request is non-keyboard lock request, we don't check for |
| // on-device model availability. |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType::POINTER_LOCK_REQUESTED, |
| /*is_on_device_model_available=*/std::nullopt, |
| /*model_has_successful_response=*/std::nullopt, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| } |
| |
| TEST_F( |
| ClientSideDetectionHostScamDetectionTest, |
| ScamExperimentVerdictOnClientPhishingResponseButDoesntShowBlockingPageDueToDisabledFlag) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionBrandAndIntentForScamDetection}, |
| {kClientSideDetectionShowScamVerdictWarning}); |
| SetInquireOnDeviceModelCallback(/*should_return_response=*/true); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/true, |
| /*expected_no_info_reason=*/std::nullopt, |
| /*expected_llama_forced_trigger_info_trigger_url=*/std::nullopt, |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::SCAM_EXPERIMENT_VERDICT_1); |
| // We do not expect the blocking page to pop up because |
| // kClientSideDetectionShowScamVerdictWarning is disabled. |
| EXPECT_CALL(*ui_manager_.get(), DisplayBlockingPage(_)).Times(0); |
| |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.0f, |
| ClientSideDetectionType::KEYBOARD_LOCK_REQUESTED, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType:: |
| KEYBOARD_LOCK_REQUESTED, |
| /*is_on_device_model_available=*/true, |
| /*model_has_successful_response=*/true, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::SCAM_EXPERIMENT_VERDICT_1); |
| } |
| |
| TEST_F(ClientSideDetectionHostScamDetectionTest, |
| ScamExperimentVerdictOnClientPhishingResponseAndShowBlockingPage) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionBrandAndIntentForScamDetection, |
| kClientSideDetectionShowScamVerdictWarning}, |
| {}); |
| SetInquireOnDeviceModelCallback(/*should_return_response=*/true); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/true, |
| /*expected_no_info_reason=*/std::nullopt, |
| /*expected_llama_forced_trigger_info_trigger_url=*/std::nullopt, |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::SCAM_EXPERIMENT_VERDICT_1); |
| // Now we run the callback to receive a server response. We do expect the |
| // blocking page to pop up on a non-phishy response with the scam experiment |
| // verdict because the feature is now enabled despite the is_phishy field is |
| // false. |
| UnsafeResource resource; |
| resource.threat_subtype = ThreatSubtype::SCAM_EXPERIMENT_VERDICT_1; |
| EXPECT_CALL(*ui_manager_.get(), |
| DisplayBlockingPage(HasScamThreatSubtype(resource))) |
| .Times(1); |
| |
| PhishingDetectionDone( |
| /*is_phishing=*/false, /*client_score=*/0.0f, |
| ClientSideDetectionType::KEYBOARD_LOCK_REQUESTED, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType:: |
| KEYBOARD_LOCK_REQUESTED, |
| /*is_on_device_model_available=*/true, |
| /*model_has_successful_response=*/true, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::SCAM_EXPERIMENT_VERDICT_1); |
| } |
| |
| TEST_F(ClientSideDetectionHostScamDetectionTest, |
| RTLookupResponseLlamaForcedTriggerInfoTriggersOnDeviceLLM) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionBrandAndIntentForScamDetection, |
| kClientSideDetectionSendLlamaForcedTriggerInfo, |
| kClientSideDetectionLlamaForcedTriggerInfoForScamDetection}, |
| {}); |
| CacheForcedTriggerInfo( |
| /*has_llama_forced_trigger_info=*/true, |
| /*intelligent_scan=*/true, |
| /*cache_expression=*/example_url_.GetContent()); |
| SetInquireOnDeviceModelCallback(/*should_return_response=*/true); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/true, |
| /*expected_no_info_reason=*/std::nullopt, |
| /*expected_llama_forced_trigger_info_trigger_url=*/ |
| example_url_.GetContent(), |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| |
| // Although the phishing detection done is set to TRIGGER_MODELS, it will |
| // eventually switch to FORCE_REQUEST because the verdict cache manager |
| // contains a suspicious RTLookupResponse. |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.8f, |
| ClientSideDetectionType::TRIGGER_MODELS, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType::FORCE_REQUEST, |
| /*is_on_device_model_available=*/true, |
| /*model_has_successful_response=*/true, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| VerifyForcedTriggerScamDetectionHistograms( |
| /*force_request=*/true, /*has_llama_forced_trigger_info=*/true, |
| /*intelligent_scan=*/true, |
| /*redirect_chain_contains_llama_forced_trigger_info=*/std::nullopt); |
| } |
| |
| TEST_F(ClientSideDetectionHostScamDetectionTest, |
| RTLookupResponseHaveFalseIntelligentScanSoItDoesNotTriggersOnDeviceLLM) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionBrandAndIntentForScamDetection, |
| kClientSideDetectionSendLlamaForcedTriggerInfo, |
| kClientSideDetectionLlamaForcedTriggerInfoForScamDetection}, |
| {}); |
| CacheForcedTriggerInfo( |
| /*has_llama_forced_trigger_info=*/true, |
| /*intelligent_scan=*/false, |
| /*cache_expression=*/example_url_.GetContent()); |
| // Because the RTLookupResponse does contain the LlamaForcedTriggerInfo but |
| // intelligent_scan field is set to false, we will not inquire the on device |
| // model. |
| EXPECT_CALL(*csd_service_, InquireOnDeviceModel(_, _)).Times(0); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/false, |
| /*expected_no_info_reason=*/std::nullopt, |
| /*expected_llama_forced_trigger_info_trigger_url=*/ |
| example_url_.GetContent(), |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| |
| // Although the phishing detection done is set to TRIGGER_MODELS, it will |
| // eventually switch to FORCE_REQUEST because the verdict cache manager |
| // contains a suspicious RTLookupResponse. |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.8f, |
| ClientSideDetectionType::TRIGGER_MODELS, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| // We do not check for on-device model availability if LLAMA force request |
| // does not request it initially. |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType::FORCE_REQUEST, |
| /*is_on_device_model_available=*/std::nullopt, |
| /*model_has_successful_response=*/std::nullopt, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| VerifyForcedTriggerScamDetectionHistograms( |
| /*force_request=*/true, /*has_llama_forced_trigger_info=*/true, |
| /*intelligent_scan=*/false, |
| /*redirect_chain_contains_llama_forced_trigger_info=*/std::nullopt); |
| } |
| |
| TEST_F( |
| ClientSideDetectionHostScamDetectionTest, |
| RTLookupResponseDoesNotHaveLlamaForcedTriggerInfoSoItDoesNotTriggersOnDeviceLLM) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionBrandAndIntentForScamDetection, |
| kClientSideDetectionSendLlamaForcedTriggerInfo, |
| kClientSideDetectionLlamaForcedTriggerInfoForScamDetection}, |
| {}); |
| CacheForcedTriggerInfo( |
| /*has_llama_forced_trigger_info=*/false, |
| /*intelligent_scan=*/false, |
| /*cache_expression=*/example_url_.GetContent()); |
| // Because the RTLookupResponse does not contain the LlamaForcedTriggerInfo at |
| // all and it wasn't found in the cache, we will not inquire the on device |
| // model. |
| EXPECT_CALL(*csd_service_, InquireOnDeviceModel(_, _)).Times(0); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/false, |
| /*expected_no_info_reason=*/std::nullopt, |
| /*expected_llama_forced_trigger_info_trigger_url=*/std::nullopt, |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| |
| // Although the phishing detection done is set to TRIGGER_MODELS, it will |
| // eventually switch to FORCE_REQUEST because the verdict cache manager |
| // contains a suspicious RTLookupResponse. |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.8f, |
| ClientSideDetectionType::TRIGGER_MODELS, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType::FORCE_REQUEST, |
| /*is_on_device_model_available=*/std::nullopt, |
| /*model_has_successful_response=*/std::nullopt, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| VerifyForcedTriggerScamDetectionHistograms( |
| /*force_request=*/true, /*has_llama_forced_trigger_info=*/false, |
| /*intelligent_scan=*/false, |
| /*redirect_chain_contains_llama_forced_trigger_info=*/std::nullopt); |
| } |
| |
| TEST_F( |
| ClientSideDetectionHostScamDetectionTest, |
| RedirectChainContainsRTLookupResponseLlamaForcedTriggerInfoSoItTriggersOnDeviceLLM) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionSendLlamaForcedTriggerInfo, |
| kClientSideDetectionLlamaForcedTriggerInfoForScamDetection}, |
| {}); |
| |
| GURL first_url_redirect("http://firsturlsuspicious.com/"); |
| GURL second_url_redirect("http://secondurlnotsuspicious.com/"); |
| GURL third_url_redirect("http://thirdurlnotsuspicious.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(first_url_redirect, false); |
| database_manager_->SetAllowlistLookupDetailsForUrl(second_url_redirect, |
| false); |
| database_manager_->SetAllowlistLookupDetailsForUrl(third_url_redirect, false); |
| |
| auto navigation = content::NavigationSimulator::CreateBrowserInitiated( |
| first_url_redirect, web_contents()); |
| navigation->Start(); |
| navigation->Redirect(second_url_redirect); |
| navigation->Redirect(third_url_redirect); |
| navigation->Commit(); |
| |
| content::NavigationEntry* entry = |
| web_contents()->GetController().GetVisibleEntry(); |
| |
| ASSERT_TRUE(entry); |
| EXPECT_EQ(entry->GetRedirectChain().size(), 3u); |
| |
| // Set the example url to the first in the redirect chain so that the cache is |
| // done for the first URL. |
| SetExampleUrl(first_url_redirect); |
| CacheForcedTriggerInfo(/*has_llama_forced_trigger_info=*/true, |
| /*intelligent_scan=*/true, |
| /*cache_expression=*/first_url_redirect.GetContent()); |
| |
| SetInquireOnDeviceModelCallback(/*should_return_response=*/true); |
| |
| // Re-set the example URL to the final url in the redirect chain. |
| SetExampleUrl(third_url_redirect); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/true, |
| /*expected_no_info_reason=*/std::nullopt, |
| /*expected_llama_forced_trigger_info_trigger_url=*/ |
| first_url_redirect.GetContent(), |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| |
| // Although the phishing detection done is set to TRIGGER_MODELS, it will |
| // eventually switch to FORCE_REQUEST because the verdict cache manager |
| // contains a suspicious RTLookupResponse. |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.8f, |
| ClientSideDetectionType::TRIGGER_MODELS, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType::FORCE_REQUEST, |
| /*is_on_device_model_available=*/true, |
| /*model_has_successful_response=*/true, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| VerifyForcedTriggerScamDetectionHistograms( |
| /*force_request=*/true, /*has_llama_forced_trigger_info=*/true, |
| /*intelligent_scan=*/true, |
| /*redirect_chain_contains_llama_forced_trigger_info=*/true); |
| } |
| |
| TEST_F(ClientSideDetectionHostScamDetectionTest, |
| RedirectChainDoesNotContainRTLookupResponseLlamaForcedTriggerInfo) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionSendLlamaForcedTriggerInfo, |
| kClientSideDetectionLlamaForcedTriggerInfoForScamDetection}, |
| {}); |
| |
| GURL first_url_redirect("http://firsturlnotsuspicious.com/"); |
| GURL second_url_redirect("http://secondurlnotsuspicious.com/"); |
| GURL third_url_redirect("http://thirdurlnotsuspicious.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(first_url_redirect, false); |
| database_manager_->SetAllowlistLookupDetailsForUrl(second_url_redirect, |
| false); |
| database_manager_->SetAllowlistLookupDetailsForUrl(third_url_redirect, false); |
| |
| auto navigation = content::NavigationSimulator::CreateBrowserInitiated( |
| first_url_redirect, web_contents()); |
| navigation->Start(); |
| navigation->Redirect(second_url_redirect); |
| navigation->Redirect(third_url_redirect); |
| navigation->Commit(); |
| |
| content::NavigationEntry* entry = |
| web_contents()->GetController().GetVisibleEntry(); |
| |
| ASSERT_TRUE(entry); |
| EXPECT_EQ(entry->GetRedirectChain().size(), 3u); |
| |
| // Set the example url to the first in the redirect chain so that the cache is |
| // done for the first URL. The force request will exist, but the |
| // LlamaForcedTriggerInfo will not. |
| SetExampleUrl(first_url_redirect); |
| CacheForcedTriggerInfo(/*has_llama_forced_trigger_info=*/false, |
| /*intelligent_scan=*/false, |
| /*cache_expression=*/first_url_redirect.GetContent()); |
| |
| // Re-set the example URL to the final url in the redirect chain. |
| SetExampleUrl(third_url_redirect); |
| |
| // Because there is no forced trigger info in the first URL in the referrer |
| // chain either, there won't be any on-device model calls. |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/false, |
| /*expected_no_info_reason=*/std::nullopt, |
| /*expected_llama_forced_trigger_info_trigger_url=*/std::nullopt, |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| |
| // Although the phishing detection done is set to TRIGGER_MODELS, it will |
| // eventually switch to FORCE_REQUEST because the verdict cache manager |
| // contains a suspicious RTLookupResponse. |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.8f, |
| ClientSideDetectionType::TRIGGER_MODELS, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType::FORCE_REQUEST, |
| /*is_on_device_model_available=*/std::nullopt, |
| /*model_has_successful_response=*/std::nullopt, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| VerifyForcedTriggerScamDetectionHistograms( |
| /*force_request=*/true, |
| /*has_llama_forced_trigger_info=*/false, |
| /*intelligent_scan=*/false, |
| /*redirect_chain_contains_llama_forced_trigger_info=*/false); |
| } |
| |
| TEST_F( |
| ClientSideDetectionHostScamDetectionTest, |
| RedirectChainDoesContainRTLookupResponseLlamaForcedTriggerInfoButKillswitchIsEnabled) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionForcedLlamaRedirectChainKillswitch, |
| kClientSideDetectionSendLlamaForcedTriggerInfo, |
| kClientSideDetectionLlamaForcedTriggerInfoForScamDetection}, |
| {}); |
| |
| GURL first_url_redirect("http://firsturlnotsuspicious.com/"); |
| GURL second_url_redirect("http://secondurlnotsuspicious.com/"); |
| GURL third_url_redirect("http://thirdurlnotsuspicious.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(first_url_redirect, false); |
| database_manager_->SetAllowlistLookupDetailsForUrl(second_url_redirect, |
| false); |
| database_manager_->SetAllowlistLookupDetailsForUrl(third_url_redirect, false); |
| |
| auto navigation = content::NavigationSimulator::CreateBrowserInitiated( |
| first_url_redirect, web_contents()); |
| navigation->Start(); |
| navigation->Redirect(second_url_redirect); |
| navigation->Redirect(third_url_redirect); |
| navigation->Commit(); |
| |
| content::NavigationEntry* entry = |
| web_contents()->GetController().GetVisibleEntry(); |
| |
| ASSERT_TRUE(entry); |
| EXPECT_EQ(entry->GetRedirectChain().size(), 3u); |
| |
| // Set the example url to the first in the redirect chain so that the cache is |
| // done for the first URL. The force request will exist and the |
| // LlamaForcedTriggerInfo as well, but due to killswitch, it won't matter. |
| SetExampleUrl(first_url_redirect); |
| CacheForcedTriggerInfo(/*has_llama_forced_trigger_info=*/true, |
| /*intelligent_scan=*/true, |
| /*cache_expression=*/first_url_redirect.GetContent()); |
| |
| // Re-set the example URL to the final url in the redirect chain. |
| SetExampleUrl(third_url_redirect); |
| |
| // There is a LlamaForcedTriggerInfo, but due to the killswitch, there won't |
| // be any on-device model calls. |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/false, |
| /*expected_no_info_reason=*/std::nullopt, |
| /*expected_llama_forced_trigger_info_trigger_url=*/std::nullopt, |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| |
| // Although the phishing detection done is set to TRIGGER_MODELS, it will |
| // eventually switch to FORCE_REQUEST because the verdict cache manager |
| // contains a suspicious RTLookupResponse. |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.8f, |
| ClientSideDetectionType::TRIGGER_MODELS, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType::FORCE_REQUEST, |
| /*is_on_device_model_available=*/std::nullopt, |
| /*model_has_successful_response=*/std::nullopt, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE); |
| |
| // Due to the killswitch, there will be no LlamaForcedTriggerInfo found in the |
| // redirect chain. |
| VerifyForcedTriggerScamDetectionHistograms( |
| /*force_request=*/true, |
| /*has_llama_forced_trigger_info=*/false, |
| /*intelligent_scan=*/true, |
| /*redirect_chain_contains_llama_forced_trigger_info=*/std::nullopt); |
| } |
| |
| TEST_F( |
| ClientSideDetectionHostTest, |
| FullscreenApiCallChecksAllowlistInPreClassificationAndDoesNotProceedWithClassification) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| std::vector<base::test::FeatureRef> enabled_features = {}; |
| enabled_features.push_back(kClientSideDetectionAcceptHCAllowlist); |
| SetFeatures(enabled_features, {}); |
| |
| csd_host_->set_high_confidence_allowlist_acceptance_rate_for_testing(1.0f); |
| base::HistogramTester histogram_tester; |
| |
| GURL url("http://host.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, /*match=*/true); |
| ExpectPreClassificationChecks(url, &kFalse, &kFalse, nullptr, nullptr, |
| nullptr); |
| NavigateAndKeepLoading(web_contents(), url); |
| WaitAndCheckPreClassificationChecks(); |
| |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.MatchHighConfidenceAllowlist.TriggerModel", 1); |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.PreClassificationCheckResult", |
| PreClassificationCheckResult::NO_CLASSIFY_MATCH_HC_ALLOWLIST, 1); |
| |
| ExpectPreClassificationChecks(url, &kFalse, &kFalse, nullptr, nullptr, |
| nullptr); |
| csd_host_->DidToggleFullscreenModeForTab(false, false); |
| WaitAndCheckPreClassificationChecks(); |
| |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.MatchCSDAllowlistOnFullscreenApi", 1); |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.MatchHighConfidenceAllowlist.FullscreenApi", 1); |
| histogram_tester.ExpectBucketCount( |
| "SBClientPhishing.PreClassificationCheckResult.FullscreenApi", |
| PreClassificationCheckResult::NO_CLASSIFY_ALLOWLIST_METRIC, 1); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| TwoFullscreenApiTriggersOnSamePageOnlyLogsOnePreclassificationCheck) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| base::HistogramTester histogram_tester; |
| |
| GURL url("http://host.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, /*match=*/true); |
| ExpectPreClassificationChecks(url, nullptr, nullptr, nullptr, nullptr, |
| nullptr); |
| csd_host_->DidToggleFullscreenModeForTab(false, false); |
| WaitAndCheckPreClassificationChecks(); |
| |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.PreClassificationCheckResult.FullscreenApi", 1); |
| |
| // We do not expect preclassification checks this time because we've done it |
| // already on the same page. |
| csd_host_->DidToggleFullscreenModeForTab(false, false); |
| |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.PreClassificationCheckResult.FullscreenApi", 1); |
| } |
| |
| TEST_F(ClientSideDetectionHostTest, |
| TwoKeyboardLockRequestsOnSamePageOnlyLogsOnePreclassificationCheck) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetEnhancedProtectionPrefForTests(profile()->GetPrefs(), true); |
| |
| base::HistogramTester histogram_tester; |
| |
| GURL url("http://host3.com/"); |
| database_manager_->SetAllowlistLookupDetailsForUrl(url, true); |
| |
| // Keyboard lock request incoming, which triggers preclassification checks. |
| ExpectPreClassificationChecks( |
| /*url=*/url, /*is_private=*/&kFalse, |
| /*match_csd_allowlist=*/nullptr, /*get_valid_cached_result=*/nullptr, |
| /*over_phishing_report_limit=*/nullptr, /*is_local=*/nullptr); |
| |
| csd_host_->KeyboardLockRequested(); |
| WaitAndCheckPreClassificationChecks(); |
| |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.PreClassificationCheckResult.KeyboardLockRequested", 1); |
| |
| // We trigger keyboard lock again, but because we're still on the same page, |
| // we do not trigger preclassification again. |
| csd_host_->KeyboardLockRequested(); |
| |
| histogram_tester.ExpectTotalCount( |
| "SBClientPhishing.PreClassificationCheckResult.KeyboardLockRequested", 1); |
| } |
| |
| TEST_F( |
| ClientSideDetectionHostScamDetectionTest, |
| RTLookupResponseLlamaForcedTriggerInfoTriggersOnDeviceLLMAndShowWarning) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionSendLlamaForcedTriggerInfo, |
| kClientSideDetectionLlamaForcedTriggerInfoForScamDetection, |
| kClientSideDetectionShowLlamaScamVerdictWarning}, |
| {}); |
| CacheForcedTriggerInfo( |
| /*has_llama_forced_trigger_info=*/true, |
| /*intelligent_scan=*/true, |
| /*cache_expression=*/example_url_.GetContent()); |
| SetInquireOnDeviceModelCallback(/*should_return_response=*/true); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/true, |
| /*expected_no_info_reason=*/std::nullopt, |
| /*expected_llama_forced_trigger_info_trigger_url=*/ |
| example_url_.GetContent(), |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::SCAM_EXPERIMENT_VERDICT_2); |
| |
| UnsafeResource resource; |
| resource.threat_subtype = ThreatSubtype::SCAM_EXPERIMENT_VERDICT_2; |
| // We do expect the blocking page to pop up on a non-phishy response with the |
| // scam experiment verdict because |
| // kClientSideDetectionShowLlamaScamVerdictWarning is now enabled despite the |
| // is_phishy field is false. |
| EXPECT_CALL(*ui_manager_.get(), |
| DisplayBlockingPage(HasScamThreatSubtype(resource))) |
| .Times(1); |
| |
| // Although the phishing detection done is set to TRIGGER_MODELS, it will |
| // eventually switch to FORCE_REQUEST because the verdict cache manager |
| // contains a suspicious RTLookupResponse. |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.8f, |
| ClientSideDetectionType::TRIGGER_MODELS, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType::FORCE_REQUEST, |
| /*is_on_device_model_available=*/true, |
| /*model_has_successful_response=*/true, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::SCAM_EXPERIMENT_VERDICT_2); |
| VerifyForcedTriggerScamDetectionHistograms( |
| /*force_request=*/true, /*has_llama_forced_trigger_info=*/true, |
| /*intelligent_scan=*/true, |
| /*redirect_chain_contains_llama_forced_trigger_info=*/std::nullopt); |
| } |
| |
| TEST_F( |
| ClientSideDetectionHostScamDetectionTest, |
| RTLookupResponseLlamaForcedTriggerInfoTriggersOnDeviceLLMButNotShowWarningDueToDisabledStudy) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionSendLlamaForcedTriggerInfo, |
| kClientSideDetectionLlamaForcedTriggerInfoForScamDetection}, |
| {kClientSideDetectionShowLlamaScamVerdictWarning}); |
| |
| CacheForcedTriggerInfo( |
| /*has_llama_forced_trigger_info=*/true, |
| /*intelligent_scan=*/true, |
| /*cache_expression=*/example_url_.GetContent()); |
| SetInquireOnDeviceModelCallback(/*should_return_response=*/true); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/true, |
| /*expected_no_info_reason=*/std::nullopt, |
| /*expected_llama_forced_trigger_info_trigger_url=*/ |
| example_url_.GetContent(), |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::SCAM_EXPERIMENT_VERDICT_2); |
| // Now we run the callback to receive a server response. Because the study is |
| // disabled, we do NOT expect the blocking page to pop up on a non-phishy |
| // response with the scam experiment verdict. |
| EXPECT_CALL(*ui_manager_.get(), DisplayBlockingPage(_)).Times(0); |
| |
| // Although the phishing detection done is set to TRIGGER_MODELS, it will |
| // eventually switch to FORCE_REQUEST because the verdict cache manager |
| // contains a suspicious RTLookupResponse. |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.8f, |
| ClientSideDetectionType::TRIGGER_MODELS, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType::FORCE_REQUEST, |
| /*is_on_device_model_available=*/true, |
| /*model_has_successful_response=*/true, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::SCAM_EXPERIMENT_VERDICT_2); |
| VerifyForcedTriggerScamDetectionHistograms( |
| /*force_request=*/true, /*has_llama_forced_trigger_info=*/true, |
| /*intelligent_scan=*/true, |
| /*redirect_chain_contains_llama_forced_trigger_info=*/std::nullopt); |
| } |
| |
| TEST_F(ClientSideDetectionHostScamDetectionTest, |
| CatchAllScamExperimentVerdictDoesNotShowWarning) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionBrandAndIntentForScamDetection, |
| kClientSideDetectionSendLlamaForcedTriggerInfo, |
| kClientSideDetectionLlamaForcedTriggerInfoForScamDetection, |
| kClientSideDetectionShowScamVerdictWarning, |
| kClientSideDetectionShowLlamaScamVerdictWarning}, |
| {}); |
| |
| EXPECT_CALL(*csd_service_, InquireOnDeviceModel(_, _)).Times(0); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/false, |
| /*expected_no_info_reason=*/std::nullopt, |
| /*expected_llama_forced_trigger_info_trigger_url=*/std::nullopt, |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::SCAM_EXPERIMENT_CATCH_ALL_TELEMETRY); |
| // Because the callback responds with the catch all verdict, we will not show |
| // a warning. |
| EXPECT_CALL(*ui_manager_.get(), DisplayBlockingPage(_)).Times(0); |
| |
| // The verdict's is_phishing is true, so that we send a ping and await a |
| // response. |
| PhishingDetectionDone(/*is_phishing=*/true, /*client_score=*/0.8f, |
| ClientSideDetectionType::TRIGGER_MODELS, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| // Although the warning has not been displayed, we should still check that the |
| // histogram has been logged. In addition, because the client side detection |
| // type is TRIGGER_MODELS, we do not check for on device model availability. |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType::TRIGGER_MODELS, |
| /*is_on_device_model_available=*/std::nullopt, |
| /*model_has_successful_response=*/std::nullopt, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::SCAM_EXPERIMENT_CATCH_ALL_TELEMETRY); |
| } |
| |
| TEST_F(ClientSideDetectionHostScamDetectionTest, |
| CatchAllEnforcementScamExperimentVerdictDoesNotShowWarningDueToStudies) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionBrandAndIntentForScamDetection}, |
| {kClientSideDetectionSendLlamaForcedTriggerInfo, |
| kClientSideDetectionLlamaForcedTriggerInfoForScamDetection, |
| kClientSideDetectionShowScamVerdictWarning, |
| kClientSideDetectionShowLlamaScamVerdictWarning}); |
| SetInquireOnDeviceModelCallback(/*should_return_response=*/true); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/true, |
| /*expected_no_info_reason=*/std::nullopt, |
| /*expected_llama_forced_trigger_info_trigger_url=*/std::nullopt, |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::SCAM_EXPERIMENT_CATCH_ALL_ENFORCEMENT); |
| |
| // Now we run the callback to receive a server response. Because the callback |
| // responds with the catch all enforcement verdict, but the studies are |
| // disabled, we will NOT show a warning. |
| EXPECT_CALL(*ui_manager_.get(), DisplayBlockingPage(_)).Times(0); |
| |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.8f, |
| ClientSideDetectionType::KEYBOARD_LOCK_REQUESTED, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType:: |
| KEYBOARD_LOCK_REQUESTED, |
| /*is_on_device_model_available=*/true, |
| /*model_has_successful_response=*/true, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::SCAM_EXPERIMENT_CATCH_ALL_ENFORCEMENT); |
| } |
| |
| TEST_F(ClientSideDetectionHostScamDetectionTest, |
| CatchAllEnforcementScamExperimentVerdictDoesShowWarning) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| GTEST_SKIP(); |
| } |
| |
| SetFeatures({kClientSideDetectionBrandAndIntentForScamDetection, |
| kClientSideDetectionSendLlamaForcedTriggerInfo, |
| kClientSideDetectionLlamaForcedTriggerInfoForScamDetection, |
| kClientSideDetectionShowScamVerdictWarning, |
| kClientSideDetectionShowLlamaScamVerdictWarning}, |
| {}); |
| SetInquireOnDeviceModelCallback(/*should_return_response=*/true); |
| SetSendClientReportPhishingRequestCallback( |
| /*has_expected_brand_and_intent=*/true, |
| /*expected_no_info_reason=*/std::nullopt, |
| /*expected_llama_forced_trigger_info_trigger_url=*/std::nullopt, |
| /*returned_is_phishing=*/false, |
| /*returned_intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::SCAM_EXPERIMENT_CATCH_ALL_ENFORCEMENT); |
| |
| UnsafeResource resource; |
| resource.threat_subtype = |
| ThreatSubtype::SCAM_EXPERIMENT_CATCH_ALL_ENFORCEMENT; |
| // Because the callback responds with the catch all enforcement verdict, we |
| // WILL show a warning. |
| EXPECT_CALL(*ui_manager_.get(), |
| DisplayBlockingPage(HasScamThreatSubtype(resource))) |
| .Times(1); |
| |
| PhishingDetectionDone(/*is_phishing=*/false, /*client_score=*/0.8f, |
| ClientSideDetectionType::KEYBOARD_LOCK_REQUESTED, |
| /*did_match_high_confidence_allowlist=*/false); |
| |
| VerifyExpectedCalls(); |
| VerifyGeneralScamDetectionHistograms( |
| /*expected_request_type=*/ClientSideDetectionType:: |
| KEYBOARD_LOCK_REQUESTED, |
| /*is_on_device_model_available=*/true, |
| /*model_has_successful_response=*/true, |
| /*intelligent_scan_verdict=*/ |
| IntelligentScanVerdict::SCAM_EXPERIMENT_CATCH_ALL_ENFORCEMENT); |
| } |
| |
| } // namespace safe_browsing |