| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // 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/chrome_cleaner/chrome_cleaner_runner_win.h" |
| |
| #include <tuple> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/logging.h" |
| #include "base/process/launch.h" |
| #include "base/process/process.h" |
| #include "base/run_loop.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/task/post_task.h" |
| #include "base/test/multiprocess_test.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/threading/thread.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "chrome/browser/safe_browsing/chrome_cleaner/mock_chrome_cleaner_process_win.h" |
| #include "chrome/browser/safe_browsing/chrome_cleaner/srt_field_trial_win.h" |
| #include "chrome/test/base/testing_browser_process.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "chrome/test/base/testing_profile_manager.h" |
| #include "components/chrome_cleaner/public/constants/constants.h" |
| #include "components/chrome_cleaner/public/interfaces/chrome_prompt.mojom.h" |
| #include "components/chrome_cleaner/test/test_name_helper.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/test/test_browser_thread_bundle.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "testing/multiprocess_func_list.h" |
| |
| namespace safe_browsing { |
| namespace { |
| |
| using ::chrome_cleaner::ChromePromptValue; |
| using ::chrome_cleaner::mojom::ChromePrompt; |
| using ::chrome_cleaner::mojom::PromptAcceptance; |
| using ::content::BrowserThread; |
| using ::testing::Bool; |
| using ::testing::Combine; |
| using ::testing::UnorderedElementsAreArray; |
| using ::testing::Values; |
| using ChromeMetricsStatus = ChromeCleanerRunner::ChromeMetricsStatus; |
| using ExtensionCleaningFeatureStatus = |
| MockChromeCleanerProcess::ExtensionCleaningFeatureStatus; |
| using UwsFoundStatus = MockChromeCleanerProcess::UwsFoundStatus; |
| |
| enum class ReporterEngine { |
| kUnspecified, |
| kOldEngine, |
| kNewEngine, |
| }; |
| |
| // Simple test fixture that intercepts the launching of the Chrome Cleaner |
| // process and does not start a separate mock Cleaner process. It will pass an |
| // invalid process handle back to ChromeCleanerRunner. Intended for testing |
| // simple things like command line flags that Chrome sends to the Chrome Cleaner |
| // process. |
| // |
| // Parameters: |
| // - metrics_status (ChromeMetricsStatus): whether Chrome metrics reporting is |
| // enabled |
| // - reporter_engine (ReporterEngine): the type of Cleaner engine specified in |
| // the SwReporterInvocation. |
| // - cleaner_logs_enabled (bool): if logs can be collected in the cleaner |
| // process running in scanning mode. |
| // - chrome_prompt (ChromePromptValue): indicates if this is a user-initiated |
| // run or if the user was prompted. |
| // - quarantine_enabled (bool): indicates if the quarantine feature is enabled. |
| class ChromeCleanerRunnerSimpleTest |
| : public testing::TestWithParam< |
| std::tuple<ChromeCleanerRunner::ChromeMetricsStatus, |
| ReporterEngine, |
| bool, |
| ChromePromptValue, |
| bool>>, |
| public ChromeCleanerRunnerTestDelegate { |
| public: |
| ChromeCleanerRunnerSimpleTest() |
| : command_line_(base::CommandLine::NO_PROGRAM) {} |
| |
| void SetUp() override { |
| std::tie(metrics_status_, reporter_engine_, cleaner_logs_enabled_, |
| chrome_prompt_, quarantine_enabled_) = GetParam(); |
| |
| std::vector<base::Feature> enabled_features; |
| if (quarantine_enabled_) { |
| enabled_features.push_back(kChromeCleanupQuarantineFeature); |
| } |
| scoped_feature_list_.InitWithFeatures(enabled_features, {}); |
| |
| SetChromeCleanerRunnerTestDelegateForTesting(this); |
| } |
| |
| void CallRunChromeCleaner() { |
| base::CommandLine command_line(base::CommandLine::NO_PROGRAM); |
| SwReporterInvocation reporter_invocation(command_line); |
| switch (reporter_engine_) { |
| case ReporterEngine::kUnspecified: |
| // No engine switch. |
| break; |
| case ReporterEngine::kOldEngine: |
| reporter_invocation.mutable_command_line().AppendSwitchASCII( |
| chrome_cleaner::kEngineSwitch, "1"); |
| break; |
| case ReporterEngine::kNewEngine: |
| reporter_invocation.mutable_command_line().AppendSwitchASCII( |
| chrome_cleaner::kEngineSwitch, "2"); |
| break; |
| } |
| |
| reporter_invocation.set_cleaner_logs_upload_enabled(cleaner_logs_enabled_); |
| |
| reporter_invocation.set_chrome_prompt(chrome_prompt_); |
| |
| ChromeCleanerRunner::RunChromeCleanerAndReplyWithExitCode( |
| /*extension_service=*/nullptr, |
| base::FilePath(FILE_PATH_LITERAL("cleaner.exe")), reporter_invocation, |
| metrics_status_, |
| base::BindOnce(&ChromeCleanerRunnerSimpleTest::OnPromptUser, |
| base::Unretained(this)), |
| base::BindOnce(&ChromeCleanerRunnerSimpleTest::OnConnectionClosed, |
| base::Unretained(this)), |
| base::BindOnce(&ChromeCleanerRunnerSimpleTest::OnProcessDone, |
| base::Unretained(this)), |
| base::ThreadTaskRunnerHandle::Get()); |
| } |
| |
| // ChromeCleanerRunnerTestDelegate overrides. |
| |
| base::Process LaunchTestProcess( |
| const base::CommandLine& command_line, |
| const base::LaunchOptions& launch_options) override { |
| command_line_ = command_line; |
| // Return an invalid process. |
| return base::Process(); |
| } |
| |
| void OnCleanerProcessDone( |
| const ChromeCleanerRunner::ProcessStatus& process_status) override {} |
| |
| // IPC callbacks. |
| |
| void OnPromptUser(ChromeCleanerScannerResults&& scanner_results, |
| ChromePrompt::PromptUserCallback response) {} |
| |
| void OnConnectionClosed() {} |
| |
| void OnProcessDone(ChromeCleanerRunner::ProcessStatus process_status) { |
| on_process_done_called_ = true; |
| process_status_ = process_status; |
| run_loop_.QuitWhenIdle(); |
| } |
| |
| protected: |
| content::TestBrowserThreadBundle test_browser_thread_bundle_; |
| |
| // Test fixture parameters. |
| ChromeCleanerRunner::ChromeMetricsStatus metrics_status_; |
| ReporterEngine reporter_engine_; |
| bool cleaner_logs_enabled_ = false; |
| ChromePromptValue chrome_prompt_ = ChromePromptValue::kUnspecified; |
| bool quarantine_enabled_ = false; |
| |
| // Set by LaunchTestProcess. |
| base::CommandLine command_line_; |
| |
| // Variables set by OnProcessDone(). |
| bool on_process_done_called_ = false; |
| ChromeCleanerRunner::ProcessStatus process_status_; |
| |
| base::RunLoop run_loop_; |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| TEST_P(ChromeCleanerRunnerSimpleTest, LaunchParams) { |
| CallRunChromeCleaner(); |
| run_loop_.Run(); |
| |
| EXPECT_TRUE(on_process_done_called_); |
| |
| EXPECT_EQ( |
| command_line_.GetSwitchValueASCII(chrome_cleaner::kExecutionModeSwitch), |
| base::IntToString( |
| static_cast<int>(chrome_cleaner::ExecutionMode::kScanning))); |
| |
| // Ensure that the engine flag is always set and that it correctly reflects |
| // the value of the same flag in the SwReporterInvocation() that was passed to |
| // ChromeCleanerRunner::RunChromeCleanerAndReplyWithExitCode(). In the tests, |
| // the engine flag in the SwReporterInvocation is controlled by the value of |
| // |reporter_engine_|. |
| // |
| // If the engine switch is missing in reporter invocation, it should still be |
| // explicitly set to the value "1" for the Cleaner. |
| std::string expected_engine_switch = |
| reporter_engine_ == ReporterEngine::kNewEngine ? "2" : "1"; |
| EXPECT_EQ(command_line_.GetSwitchValueASCII(chrome_cleaner::kEngineSwitch), |
| expected_engine_switch); |
| |
| EXPECT_EQ(metrics_status_ == ChromeMetricsStatus::kEnabled, |
| command_line_.HasSwitch(chrome_cleaner::kUmaUserSwitch)); |
| EXPECT_EQ( |
| metrics_status_ == ChromeMetricsStatus::kEnabled, |
| command_line_.HasSwitch(chrome_cleaner::kEnableCrashReportingSwitch)); |
| EXPECT_EQ( |
| cleaner_logs_enabled_, |
| command_line_.HasSwitch(chrome_cleaner::kWithScanningModeLogsSwitch)); |
| EXPECT_EQ( |
| command_line_.GetSwitchValueASCII(chrome_cleaner::kChromePromptSwitch), |
| base::IntToString(static_cast<int>(chrome_prompt_))); |
| |
| const std::string reboot_prompt_method = command_line_.GetSwitchValueASCII( |
| chrome_cleaner::kRebootPromptMethodSwitch); |
| int reboot_prompt = -1; |
| EXPECT_TRUE(base::StringToInt(reboot_prompt_method, &reboot_prompt)); |
| |
| EXPECT_EQ(quarantine_enabled_, |
| command_line_.HasSwitch(chrome_cleaner::kQuarantineSwitch)); |
| } |
| |
| INSTANTIATE_TEST_CASE_P( |
| All, |
| ChromeCleanerRunnerSimpleTest, |
| Combine(Values(ChromeCleanerRunner::ChromeMetricsStatus::kEnabled, |
| ChromeCleanerRunner::ChromeMetricsStatus::kDisabled), |
| Values(ReporterEngine::kUnspecified, |
| ReporterEngine::kOldEngine, |
| ReporterEngine::kNewEngine), |
| Bool(), |
| Values(ChromePromptValue::kPrompted, |
| ChromePromptValue::kUserInitiated), |
| Bool())); |
| |
| typedef std::tuple<UwsFoundStatus, |
| ExtensionCleaningFeatureStatus, |
| MockChromeCleanerProcess::ItemsReporting, |
| MockChromeCleanerProcess::ItemsReporting, |
| MockChromeCleanerProcess::CrashPoint, |
| PromptAcceptance> |
| ChromeCleanerRunnerTestParams; |
| |
| // Test fixture for testing ChromeCleanerRunner with a mock Chrome Cleaner |
| // process. |
| class ChromeCleanerRunnerTest |
| : public testing::TestWithParam<ChromeCleanerRunnerTestParams>, |
| public ChromeCleanerRunnerTestDelegate { |
| public: |
| ChromeCleanerRunnerTest() |
| : profile_manager_(TestingBrowserProcess::GetGlobal()) {} |
| ~ChromeCleanerRunnerTest() override {} |
| |
| void SetUp() override { |
| // Set up the testing profile, so chrome_cleaner_scanner_results_win can get |
| // the extensions registry from it. |
| ASSERT_TRUE(profile_manager_.SetUp()); |
| testing_profile_ = profile_manager_.CreateTestingProfile("Profile 1"); |
| MockChromeCleanerProcess::AddMockExtensionsToProfile(testing_profile_); |
| |
| UwsFoundStatus uws_found_state; |
| MockChromeCleanerProcess::ItemsReporting registry_keys_reporting; |
| MockChromeCleanerProcess::ItemsReporting extensions_reporting; |
| MockChromeCleanerProcess::CrashPoint crash_point; |
| std::tie(uws_found_state, extension_cleaning_feature_status_, |
| registry_keys_reporting, extensions_reporting, crash_point, |
| prompt_acceptance_to_send_) = GetParam(); |
| |
| ASSERT_FALSE(uws_found_state == UwsFoundStatus::kNoUwsFound && |
| prompt_acceptance_to_send_ != PromptAcceptance::DENIED); |
| |
| if (extension_cleaning_feature_status_ == |
| ExtensionCleaningFeatureStatus::kEnabled) |
| features_.InitAndEnableFeature(kChromeCleanupExtensionsFeature); |
| else |
| features_.InitAndDisableFeature(kChromeCleanupExtensionsFeature); |
| |
| cleaner_process_options_.SetReportedResults( |
| uws_found_state != UwsFoundStatus::kNoUwsFound, registry_keys_reporting, |
| extensions_reporting); |
| cleaner_process_options_.set_reboot_required( |
| uws_found_state == UwsFoundStatus::kUwsFoundRebootRequired); |
| cleaner_process_options_.set_crash_point(crash_point); |
| cleaner_process_options_.set_expected_user_response( |
| prompt_acceptance_to_send_); |
| |
| SetChromeCleanerRunnerTestDelegateForTesting(this); |
| } |
| |
| void CallRunChromeCleaner() { |
| base::CommandLine command_line(base::CommandLine::NO_PROGRAM); |
| ChromeCleanerRunner::RunChromeCleanerAndReplyWithExitCode( |
| /*extension_service=*/nullptr, |
| base::FilePath(FILE_PATH_LITERAL("cleaner.exe")), |
| SwReporterInvocation(command_line), ChromeMetricsStatus::kDisabled, |
| base::BindOnce(&ChromeCleanerRunnerTest::OnPromptUser, |
| base::Unretained(this)), |
| base::BindOnce(&ChromeCleanerRunnerTest::OnConnectionClosed, |
| base::Unretained(this)), |
| base::BindOnce(&ChromeCleanerRunnerTest::OnProcessDone, |
| base::Unretained(this)), |
| base::ThreadTaskRunnerHandle::Get()); |
| } |
| |
| // ChromeCleanerRunnerTestDelegate overrides. |
| |
| base::Process LaunchTestProcess( |
| const base::CommandLine& command_line, |
| const base::LaunchOptions& launch_options) override { |
| // Add switches and program name that the test process needs for the multi |
| // process tests. |
| base::CommandLine test_process_command_line = |
| base::GetMultiProcessTestChildBaseCommandLine(); |
| for (const auto& pair : command_line.GetSwitches()) |
| test_process_command_line.AppendSwitchNative(pair.first, pair.second); |
| |
| cleaner_process_options_.AddSwitchesToCommandLine( |
| &test_process_command_line); |
| |
| base::Process process = base::SpawnMultiProcessTestChild( |
| "MockChromeCleanerProcessMain", test_process_command_line, |
| launch_options); |
| |
| EXPECT_TRUE(process.IsValid()); |
| return process; |
| } |
| |
| void OnCleanerProcessDone( |
| const ChromeCleanerRunner::ProcessStatus& process_status) override {} |
| |
| // IPC callbacks. |
| |
| // Will receive the main Mojo message from the Mock Chrome Cleaner process. |
| void OnPromptUser(ChromeCleanerScannerResults&& scanner_results, |
| ChromePrompt::PromptUserCallback response) { |
| on_prompt_user_called_ = true; |
| received_scanner_results_ = std::move(scanner_results); |
| base::CreateSingleThreadTaskRunnerWithTraits({BrowserThread::IO}) |
| ->PostTask(FROM_HERE, base::BindOnce(std::move(response), |
| prompt_acceptance_to_send_)); |
| } |
| |
| void QuitTestRunLoopIfCommunicationDone() { |
| if (on_process_done_called_ && on_connection_closed_called_) |
| run_loop_.QuitWhenIdle(); |
| } |
| |
| void OnConnectionClosed() { |
| on_connection_closed_called_ = true; |
| QuitTestRunLoopIfCommunicationDone(); |
| } |
| |
| void OnProcessDone(ChromeCleanerRunner::ProcessStatus process_status) { |
| on_process_done_called_ = true; |
| process_status_ = process_status; |
| QuitTestRunLoopIfCommunicationDone(); |
| } |
| |
| protected: |
| content::TestBrowserThreadBundle test_browser_thread_bundle_; |
| TestingProfileManager profile_manager_; |
| TestingProfile* testing_profile_; |
| |
| base::RunLoop run_loop_; |
| |
| MockChromeCleanerProcess::Options cleaner_process_options_; |
| PromptAcceptance prompt_acceptance_to_send_ = PromptAcceptance::UNSPECIFIED; |
| ExtensionCleaningFeatureStatus extension_cleaning_feature_status_; |
| |
| // Set by OnProcessDone(). |
| ChromeCleanerRunner::ProcessStatus process_status_; |
| |
| // Set by OnPromptUser(). |
| ChromeCleanerScannerResults received_scanner_results_; |
| |
| bool on_prompt_user_called_ = false; |
| bool on_connection_closed_called_ = false; |
| bool on_process_done_called_ = false; |
| |
| base::test::ScopedFeatureList features_; |
| }; |
| |
| MULTIPROCESS_TEST_MAIN(MockChromeCleanerProcessMain) { |
| base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); |
| MockChromeCleanerProcess::Options options; |
| EXPECT_TRUE(MockChromeCleanerProcess::Options::FromCommandLine(*command_line, |
| &options)); |
| |
| std::string chrome_mojo_pipe_token = command_line->GetSwitchValueASCII( |
| chrome_cleaner::kChromeMojoPipeTokenSwitch); |
| EXPECT_FALSE(chrome_mojo_pipe_token.empty()); |
| |
| // Since failures in any of the above calls to EXPECT_*() do not actually fail |
| // the test, we need to ensure that we return an exit code to indicate test |
| // failure in such cases. |
| if (::testing::Test::HasFailure()) |
| return MockChromeCleanerProcess::kInternalTestFailureExitCode; |
| |
| MockChromeCleanerProcess mock_cleaner_process(options, |
| chrome_mojo_pipe_token); |
| return mock_cleaner_process.Run(); |
| } |
| |
| TEST_P(ChromeCleanerRunnerTest, WithMockCleanerProcess) { |
| CallRunChromeCleaner(); |
| run_loop_.Run(); |
| |
| EXPECT_TRUE(on_process_done_called_); |
| EXPECT_TRUE(on_connection_closed_called_); |
| EXPECT_EQ(on_prompt_user_called_, |
| (cleaner_process_options_.crash_point() == |
| MockChromeCleanerProcess::CrashPoint::kNone || |
| cleaner_process_options_.crash_point() == |
| MockChromeCleanerProcess::CrashPoint::kAfterResponseReceived)); |
| |
| if (on_prompt_user_called_ && |
| !cleaner_process_options_.files_to_delete().empty()) { |
| EXPECT_THAT( |
| received_scanner_results_.files_to_delete(), |
| UnorderedElementsAreArray(cleaner_process_options_.files_to_delete())); |
| |
| if (cleaner_process_options_.registry_keys()) { |
| EXPECT_THAT( |
| received_scanner_results_.registry_keys(), |
| UnorderedElementsAreArray(*cleaner_process_options_.registry_keys())); |
| } else { |
| EXPECT_TRUE(received_scanner_results_.registry_keys().empty()); |
| } |
| |
| std::set<base::string16> extension_names; |
| received_scanner_results_.FetchExtensionNames(testing_profile_, |
| &extension_names); |
| if (cleaner_process_options_.extension_ids() && |
| extension_cleaning_feature_status_ == |
| ExtensionCleaningFeatureStatus::kEnabled) { |
| EXPECT_THAT(extension_names, |
| UnorderedElementsAreArray( |
| *cleaner_process_options_.expected_extension_names())); |
| } else { |
| EXPECT_TRUE(extension_names.empty()); |
| } |
| } |
| |
| EXPECT_EQ(process_status_.launch_status, |
| ChromeCleanerRunner::LaunchStatus::kSuccess); |
| EXPECT_EQ( |
| process_status_.exit_code, |
| cleaner_process_options_.ExpectedExitCode(prompt_acceptance_to_send_)); |
| } |
| |
| INSTANTIATE_TEST_CASE_P( |
| NoUwsFound, |
| ChromeCleanerRunnerTest, |
| Combine( |
| Values(UwsFoundStatus::kNoUwsFound), |
| // When no UwS is found we don't care about extension removel. |
| Values(ExtensionCleaningFeatureStatus::kDisabled), |
| Values(MockChromeCleanerProcess::ItemsReporting::kUnsupported, |
| MockChromeCleanerProcess::ItemsReporting::kNotReported, |
| MockChromeCleanerProcess::ItemsReporting::kReported), |
| Values(MockChromeCleanerProcess::ItemsReporting::kUnsupported, |
| MockChromeCleanerProcess::ItemsReporting::kNotReported, |
| MockChromeCleanerProcess::ItemsReporting::kReported), |
| Values(MockChromeCleanerProcess::CrashPoint::kNone, |
| MockChromeCleanerProcess::CrashPoint::kOnStartup, |
| MockChromeCleanerProcess::CrashPoint::kAfterConnection, |
| MockChromeCleanerProcess::CrashPoint::kAfterRequestSent, |
| MockChromeCleanerProcess::CrashPoint::kAfterResponseReceived), |
| Values(PromptAcceptance::DENIED)), |
| chrome_cleaner::GetParamNameForTest()); |
| |
| INSTANTIATE_TEST_CASE_P( |
| UwsFound, |
| ChromeCleanerRunnerTest, |
| Combine( |
| Values(UwsFoundStatus::kUwsFoundRebootRequired, |
| UwsFoundStatus::kUwsFoundNoRebootRequired), |
| Values(ExtensionCleaningFeatureStatus::kEnabled, |
| ExtensionCleaningFeatureStatus::kDisabled), |
| Values(MockChromeCleanerProcess::ItemsReporting::kUnsupported, |
| MockChromeCleanerProcess::ItemsReporting::kNotReported, |
| MockChromeCleanerProcess::ItemsReporting::kReported), |
| Values(MockChromeCleanerProcess::ItemsReporting::kUnsupported, |
| MockChromeCleanerProcess::ItemsReporting::kNotReported, |
| MockChromeCleanerProcess::ItemsReporting::kReported), |
| Values(MockChromeCleanerProcess::CrashPoint::kNone, |
| MockChromeCleanerProcess::CrashPoint::kOnStartup, |
| MockChromeCleanerProcess::CrashPoint::kAfterConnection, |
| MockChromeCleanerProcess::CrashPoint::kAfterRequestSent, |
| MockChromeCleanerProcess::CrashPoint::kAfterResponseReceived), |
| Values(PromptAcceptance::DENIED, |
| PromptAcceptance::ACCEPTED_WITH_LOGS, |
| PromptAcceptance::ACCEPTED_WITHOUT_LOGS)), |
| chrome_cleaner::GetParamNameForTest()); |
| |
| } // namespace |
| } // namespace safe_browsing |