blob: 2bef02fd421af1fd2d58e04fb09d9c481507c34d [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/safe_browsing/phishy_interaction_tracker.h"
#include <string>
#include <utility>
#include "base/metrics/statistics_recorder.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "build/build_config.h"
#include "chrome/browser/safe_browsing/chrome_ping_manager_factory.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/test_safe_browsing_service.h"
#include "chrome/common/url_constants.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_browser_process.h"
#include "components/safe_browsing/content/browser/content_unsafe_resource_util.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/mock_render_process_host.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
#include "services/network/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
using content::WebContents;
using safe_browsing::PhishyInteractionTracker;
using testing::_;
using testing::Mock;
using testing::StrictMock;
namespace {
constexpr char kBadURL[] = "https://www.phishing.com";
class MockSafeBrowsingUIManager : public safe_browsing::SafeBrowsingUIManager {
public:
MockSafeBrowsingUIManager()
: SafeBrowsingUIManager(
std::make_unique<
safe_browsing::ChromeSafeBrowsingUIManagerDelegate>(),
std::make_unique<
safe_browsing::ChromeSafeBrowsingBlockingPageFactory>(),
GURL(chrome::kChromeUINewTabURL)) {}
MockSafeBrowsingUIManager(const MockSafeBrowsingUIManager&) = delete;
MockSafeBrowsingUIManager& operator=(const MockSafeBrowsingUIManager&) =
delete;
MOCK_METHOD1(DisplayBlockingPage, void(const UnsafeResource& resource));
bool IsUrlAllowlistedOrPendingForWebContents(
const GURL& url,
content::NavigationEntry* entry,
WebContents* web_contents,
bool allowlist_only,
safe_browsing::SBThreatType* threat_type) override {
*threat_type = safe_browsing::SBThreatType::SB_THREAT_TYPE_URL_PHISHING;
return true;
}
protected:
~MockSafeBrowsingUIManager() override = default;
};
} // namespace
class PhishyInteractionTrackerTest : public ChromeRenderViewHostTestHarness {
public:
typedef security_interstitials::UnsafeResource UnsafeResource;
PhishyInteractionTrackerTest() = default;
~PhishyInteractionTrackerTest() override = default;
void SetUp() override {
browser_process_ = TestingBrowserProcess::GetGlobal();
sb_service_ =
base::MakeRefCounted<safe_browsing::TestSafeBrowsingService>();
sb_service_->SetUseTestUrlLoaderFactory(true);
// Set sb_service before the ChromeRenderViewHostTestHarness::SetUp(),
// because it is needed to construct ping manager.
browser_process_->SetSafeBrowsingService(sb_service_.get());
ChromeRenderViewHostTestHarness::SetUp();
ui_manager_ = new StrictMock<MockSafeBrowsingUIManager>();
phishy_interaction_tracker_ =
base::WrapUnique(new PhishyInteractionTracker(web_contents()));
phishy_interaction_tracker_->SetUIManagerForTesting(ui_manager_.get());
phishy_interaction_tracker_->HandlePageChanged();
}
void TearDown() override {
browser_process_->SetSafeBrowsingService(nullptr);
// Delete the tracker object on the UI thread and release the
// SafeBrowsingService.
sb_service_.reset();
ui_manager_.reset();
phishy_interaction_tracker_.reset();
ChromeRenderViewHostTestHarness::TearDown();
}
Profile* profile() {
return Profile::FromBrowserContext(web_contents()->GetBrowserContext());
}
security_interstitials::UnsafeResource MakeUnsafeResource(const char* url) {
security_interstitials::UnsafeResource resource;
resource.url = GURL(url);
resource.threat_type =
safe_browsing::SBThreatType::SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING;
return resource;
}
std::unique_ptr<safe_browsing::ClientSafeBrowsingReportRequest>
GetActualRequest(const network::ResourceRequest& request) {
std::string request_string = GetUploadData(request);
auto actual_request =
std::make_unique<safe_browsing::ClientSafeBrowsingReportRequest>();
actual_request->ParseFromString(request_string);
return actual_request;
}
void TriggerClickEvent() {
blink::WebMouseEvent mouse_event = blink::WebMouseEvent(
blink::WebInputEvent::Type::kMouseDown, gfx::PointF(), gfx::PointF(),
blink::WebPointerProperties::Button::kBack, 0, 0,
base::TimeTicks::Now());
phishy_interaction_tracker_->HandleInputEvent(mouse_event);
}
void TriggerKeyEvent() {
input::NativeWebKeyboardEvent key_event(
blink::WebKeyboardEvent::Type::kChar,
blink::WebInputEvent::kNoModifiers, base::TimeTicks::Now());
phishy_interaction_tracker_->HandleInputEvent(key_event);
}
void TriggerPasteEvent() { phishy_interaction_tracker_->HandlePasteEvent(); }
void SetNullDelayForTest() {
phishy_interaction_tracker_->SetInactivityDelayForTesting(
base::Milliseconds(0));
}
void VerifyInteraction(
const safe_browsing::ClientSafeBrowsingReportRequest& report,
const safe_browsing::ClientSafeBrowsingReportRequest::
PhishySiteInteraction::PhishySiteInteractionType&
expected_interaction_type,
const int& expected_occurrence_count) {
// Find the interaction within the report by comparing
// security_interstitial_interaction.
for (auto interaction : report.phishy_site_interactions()) {
if (interaction.phishy_site_interaction_type() ==
expected_interaction_type) {
EXPECT_EQ(interaction.occurrence_count(), expected_occurrence_count);
if (expected_occurrence_count == 1) {
EXPECT_EQ(interaction.first_interaction_timestamp_msec(),
interaction.last_interaction_timestamp_msec());
} else {
EXPECT_LE(interaction.first_interaction_timestamp_msec(),
interaction.last_interaction_timestamp_msec());
}
break;
}
}
}
void VerifyPhishyInteractionReport(
const safe_browsing::ClientSafeBrowsingReportRequest& report,
int expected_click_occurrences,
int expected_key_occurrences,
int expected_paste_occurrences) {
ASSERT_EQ(report.type(), safe_browsing::ClientSafeBrowsingReportRequest::
PHISHY_SITE_INTERACTIONS);
ASSERT_EQ(report.phishy_site_interactions().size(), 3);
VerifyInteraction(report,
safe_browsing::ClientSafeBrowsingReportRequest::
PhishySiteInteraction::PHISHY_CLICK_EVENT,
expected_click_occurrences);
VerifyInteraction(report,
safe_browsing::ClientSafeBrowsingReportRequest::
PhishySiteInteraction::PHISHY_KEY_EVENT,
expected_key_occurrences);
VerifyInteraction(report,
safe_browsing::ClientSafeBrowsingReportRequest::
PhishySiteInteraction::PHISHY_PASTE_EVENT,
expected_paste_occurrences);
}
protected:
raw_ptr<TestingBrowserProcess> browser_process_;
scoped_refptr<safe_browsing::TestSafeBrowsingService> sb_service_;
std::unique_ptr<PhishyInteractionTracker> phishy_interaction_tracker_;
scoped_refptr<MockSafeBrowsingUIManager> ui_manager_;
safe_browsing::ChromePingManagerAllowerForTesting allow_ping_manager_;
};
TEST_F(PhishyInteractionTrackerTest, CheckHistogramCountsOnPhishyUserEvents) {
base::HistogramTester histogram_tester_;
security_interstitials::UnsafeResource resource = MakeUnsafeResource(kBadURL);
safe_browsing::SBThreatType threat_type;
EXPECT_TRUE(ui_manager_->IsUrlAllowlistedOrPendingForWebContents(
resource.url, /*entry=*/nullptr,
safe_browsing::unsafe_resource_util::GetWebContentsForResource(resource),
true, &threat_type));
const std::string phishy_interaction_histogram = "SafeBrowsing.PhishySite.";
const int kExpectedClickEventCount = 3;
const int kExpectedKeyEventCount = 5;
const int kExpectedPasteEventCount = 2;
// Trigger kExpectedClickEventCount mouse events.
for (int i = 0; i < kExpectedClickEventCount; ++i) {
TriggerClickEvent();
}
// Trigger kExpectedKeyEventCount key events.
for (int i = 0; i < kExpectedKeyEventCount; ++i) {
TriggerKeyEvent();
}
// Trigger kExpectedPasteEventCount - 1 paste events so we can trigger a
// paste below.
for (int i = 0; i < kExpectedPasteEventCount - 1; ++i) {
TriggerPasteEvent();
}
// Set a null delay so that histograms get logged after this last user event.
SetNullDelayForTest();
TriggerPasteEvent();
base::RunLoop run_loop;
base::StatisticsRecorder::ScopedHistogramSampleObserver observer(
"SafeBrowsing.PhishySite.PasteEventCount", run_loop.QuitClosure());
run_loop.Run();
histogram_tester_.ExpectUniqueSample(
phishy_interaction_histogram + "ClickEventCount",
kExpectedClickEventCount, 1);
histogram_tester_.ExpectUniqueSample(
phishy_interaction_histogram + "KeyEventCount", kExpectedKeyEventCount,
1);
histogram_tester_.ExpectUniqueSample(
phishy_interaction_histogram + "PasteEventCount",
kExpectedPasteEventCount, 1);
}
TEST_F(PhishyInteractionTrackerTest, CheckPhishyUserInteractionClientReport) {
safe_browsing::SetExtendedReportingPrefForTests(profile()->GetPrefs(), true);
const int kExpectedClickEventCount = 4;
const int kExpectedKeyEventCount = 1;
const int kExpectedPasteEventCount = 3;
auto* ping_manager =
safe_browsing::ChromePingManagerFactory::GetForBrowserContext(profile());
network::TestURLLoaderFactory test_url_loader_factory;
base::RunLoop run_loop;
test_url_loader_factory.SetInterceptor(
base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
std::unique_ptr<safe_browsing::ClientSafeBrowsingReportRequest>
actual_request = GetActualRequest(request);
VerifyPhishyInteractionReport(
*actual_request.get(), kExpectedClickEventCount,
kExpectedKeyEventCount, kExpectedPasteEventCount);
run_loop.Quit();
}));
ping_manager->SetURLLoaderFactoryForTesting(
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&test_url_loader_factory));
// Trigger kExpectedClickEventCount mouse events.
for (int i = 0; i < kExpectedClickEventCount; ++i) {
TriggerClickEvent();
}
// Trigger kExpectedKeyEventCount key events.
for (int i = 0; i < kExpectedKeyEventCount; ++i) {
TriggerKeyEvent();
}
// Trigger kExpectedPasteEventCount - 1 paste events so we can trigger a
// paste below.
for (int i = 0; i < kExpectedPasteEventCount - 1; ++i) {
TriggerPasteEvent();
}
// Set a null delay so that histograms get logged after this last user event.
SetNullDelayForTest();
TriggerPasteEvent();
run_loop.Run();
}