| // 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/mock_chrome_cleaner_process_win.h" |
| |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/command_line.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/run_loop.h" |
| #include "base/sequenced_task_runner.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/scoped_task_environment.h" |
| #include "base/threading/sequenced_task_runner_handle.h" |
| #include "base/threading/thread.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/manifest.h" |
| #include "mojo/core/embedder/embedder.h" |
| #include "mojo/core/embedder/scoped_ipc_support.h" |
| #include "mojo/public/cpp/platform/platform_channel.h" |
| #include "mojo/public/cpp/system/invitation.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| namespace safe_browsing { |
| |
| namespace { |
| |
| using ::chrome_cleaner::mojom::ChromePrompt; |
| using ::chrome_cleaner::mojom::ChromePromptPtr; |
| using ::chrome_cleaner::mojom::ChromePromptPtrInfo; |
| using ::chrome_cleaner::mojom::PromptAcceptance; |
| using CrashPoint = MockChromeCleanerProcess::CrashPoint; |
| using ExtensionCleaningFeatureStatus = |
| MockChromeCleanerProcess::ExtensionCleaningFeatureStatus; |
| using ItemsReporting = MockChromeCleanerProcess::ItemsReporting; |
| using UwsFoundStatus = MockChromeCleanerProcess::UwsFoundStatus; |
| |
| constexpr char kCrashPointSwitch[] = "mock-crash-point"; |
| constexpr char kUwsFoundSwitch[] = "mock-uws-found"; |
| constexpr char kRebootRequiredSwitch[] = "mock-reboot-required"; |
| constexpr char kRegistryKeysReportingSwitch[] = "registry-keys-reporting"; |
| constexpr char kExtensionsReportingSwitch[] = "extensions-reporting"; |
| constexpr char kExpectedUserResponseSwitch[] = "mock-expected-user-response"; |
| |
| scoped_refptr<extensions::Extension> CreateExtension(const base::string16& name, |
| const base::string16& id, |
| std::string* error) { |
| base::DictionaryValue manifest; |
| manifest.SetKey("name", base::Value(name)); |
| manifest.SetKey("version", base::Value("0")); |
| manifest.SetKey("manifest_version", base::Value(2)); |
| |
| return extensions::Extension::Create(base::FilePath(), |
| extensions::Manifest::INTERNAL, manifest, |
| 0, base::UTF16ToUTF8(id), error); |
| } |
| |
| } // namespace |
| |
| const base::char16 MockChromeCleanerProcess::kInstalledExtensionId1[] = |
| L"aaaabbbbccccddddeeeeffffgggghhhh"; |
| const base::char16 MockChromeCleanerProcess::kInstalledExtensionName1[] = |
| L"Some Extension"; |
| const base::char16 MockChromeCleanerProcess::kInstalledExtensionId2[] = |
| L"ababababcdcdcdcdefefefefghghghgh"; |
| const base::char16 MockChromeCleanerProcess::kInstalledExtensionName2[] = |
| L"Another Extension"; |
| const base::char16 MockChromeCleanerProcess::kUnknownExtensionId[] = |
| L"unexpectedextensionidabcdefghijk"; |
| |
| // static |
| void MockChromeCleanerProcess::AddMockExtensionsToProfile(Profile* profile) { |
| extensions::ExtensionRegistry* extension_registry = |
| extensions::ExtensionRegistry::Get(profile); |
| scoped_refptr<extensions::Extension> extension; |
| std::string error; |
| |
| extension = |
| CreateExtension(MockChromeCleanerProcess::kInstalledExtensionName1, |
| MockChromeCleanerProcess::kInstalledExtensionId1, &error); |
| if (extension && error.empty()) { |
| extension_registry->AddEnabled(extension); |
| } else { |
| LOG(ERROR) << "Error creating mock extension: " << error; |
| } |
| |
| extension = |
| CreateExtension(MockChromeCleanerProcess::kInstalledExtensionName2, |
| MockChromeCleanerProcess::kInstalledExtensionId2, &error); |
| if (extension && error.empty()) { |
| extension_registry->AddEnabled(extension); |
| } else { |
| LOG(ERROR) << "Error creating mock extension: " << error; |
| } |
| } |
| |
| // static |
| bool MockChromeCleanerProcess::Options::FromCommandLine( |
| const base::CommandLine& command_line, |
| Options* options) { |
| int registry_keys_reporting_int = -1; |
| if (!base::StringToInt( |
| command_line.GetSwitchValueASCII(kRegistryKeysReportingSwitch), |
| ®istry_keys_reporting_int) || |
| registry_keys_reporting_int < 0 || |
| registry_keys_reporting_int >= |
| static_cast<int>(ItemsReporting::kNumItemsReporting)) { |
| return false; |
| } |
| |
| int extensions_reporting_int = -1; |
| if (!base::StringToInt( |
| command_line.GetSwitchValueASCII(kExtensionsReportingSwitch), |
| &extensions_reporting_int) || |
| extensions_reporting_int < 0 || |
| extensions_reporting_int >= |
| static_cast<int>(ItemsReporting::kNumItemsReporting)) { |
| return false; |
| } |
| |
| options->SetReportedResults( |
| command_line.HasSwitch(kUwsFoundSwitch), |
| static_cast<ItemsReporting>(registry_keys_reporting_int), |
| static_cast<ItemsReporting>(extensions_reporting_int)); |
| options->set_reboot_required(command_line.HasSwitch(kRebootRequiredSwitch)); |
| |
| if (command_line.HasSwitch(kCrashPointSwitch)) { |
| int crash_point_int = 0; |
| if (base::StringToInt(command_line.GetSwitchValueASCII(kCrashPointSwitch), |
| &crash_point_int) && |
| crash_point_int >= 0 && |
| crash_point_int < static_cast<int>(CrashPoint::kNumCrashPoints)) { |
| options->set_crash_point(static_cast<CrashPoint>(crash_point_int)); |
| } else { |
| return false; |
| } |
| } |
| |
| if (command_line.HasSwitch(kExpectedUserResponseSwitch)) { |
| int expected_response_int = 0; |
| if (base::StringToInt( |
| command_line.GetSwitchValueASCII(kExpectedUserResponseSwitch), |
| &expected_response_int) && |
| expected_response_int >= 0 && |
| expected_response_int < |
| static_cast<int>(PromptAcceptance::NUM_VALUES)) { |
| options->set_expected_user_response( |
| static_cast<PromptAcceptance>(expected_response_int)); |
| } else { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| MockChromeCleanerProcess::Options::Options() = default; |
| |
| MockChromeCleanerProcess::Options::Options(const Options& other) |
| : files_to_delete_(other.files_to_delete_), |
| registry_keys_(other.registry_keys_), |
| extension_ids_(other.extension_ids_), |
| reboot_required_(other.reboot_required_), |
| crash_point_(other.crash_point_), |
| registry_keys_reporting_(other.registry_keys_reporting_), |
| extensions_reporting_(other.extensions_reporting_), |
| expected_user_response_(other.expected_user_response_) {} |
| |
| MockChromeCleanerProcess::Options& MockChromeCleanerProcess::Options::operator=( |
| const Options& other) { |
| files_to_delete_ = other.files_to_delete_; |
| registry_keys_ = other.registry_keys_; |
| extension_ids_ = other.extension_ids_; |
| reboot_required_ = other.reboot_required_; |
| crash_point_ = other.crash_point_; |
| registry_keys_reporting_ = other.registry_keys_reporting_; |
| extensions_reporting_ = other.extensions_reporting_; |
| expected_user_response_ = other.expected_user_response_; |
| return *this; |
| } |
| |
| MockChromeCleanerProcess::Options::~Options() {} |
| |
| void MockChromeCleanerProcess::Options::AddSwitchesToCommandLine( |
| base::CommandLine* command_line) const { |
| if (!files_to_delete_.empty()) |
| command_line->AppendSwitch(kUwsFoundSwitch); |
| |
| if (reboot_required()) |
| command_line->AppendSwitch(kRebootRequiredSwitch); |
| |
| if (crash_point() != CrashPoint::kNone) { |
| command_line->AppendSwitchASCII( |
| kCrashPointSwitch, |
| base::NumberToString(static_cast<int>(crash_point()))); |
| } |
| |
| command_line->AppendSwitchASCII( |
| kRegistryKeysReportingSwitch, |
| base::NumberToString(static_cast<int>(registry_keys_reporting()))); |
| command_line->AppendSwitchASCII( |
| kExtensionsReportingSwitch, |
| base::NumberToString(static_cast<int>(extensions_reporting()))); |
| |
| if (expected_user_response() != PromptAcceptance::UNSPECIFIED) { |
| command_line->AppendSwitchASCII( |
| kExpectedUserResponseSwitch, |
| base::NumberToString(static_cast<int>(expected_user_response()))); |
| } |
| } |
| |
| void MockChromeCleanerProcess::Options::SetReportedResults( |
| bool has_files_to_remove, |
| ItemsReporting registry_keys_reporting, |
| ItemsReporting extensions_reporting) { |
| if (!has_files_to_remove) |
| files_to_delete_.clear(); |
| if (has_files_to_remove) { |
| files_to_delete_.push_back( |
| base::FilePath(FILE_PATH_LITERAL("/path/to/file1.exe"))); |
| files_to_delete_.push_back( |
| base::FilePath(FILE_PATH_LITERAL("/path/to/other/file2.exe"))); |
| files_to_delete_.push_back( |
| base::FilePath(FILE_PATH_LITERAL("/path/to/some file.dll"))); |
| } |
| |
| registry_keys_reporting_ = registry_keys_reporting; |
| switch (registry_keys_reporting) { |
| case ItemsReporting::kUnsupported: |
| // Defined as an optional object in which a registry keys vector is not |
| // present. |
| registry_keys_ = base::Optional<std::vector<base::string16>>(); |
| break; |
| |
| case ItemsReporting::kNotReported: |
| // Defined as an optional object in which an empty registry keys vector is |
| // present. |
| registry_keys_ = |
| base::Optional<std::vector<base::string16>>(base::in_place); |
| break; |
| |
| case ItemsReporting::kReported: |
| // Defined as an optional object in which a non-empty registry keys vector |
| // is present. |
| registry_keys_ = base::Optional<std::vector<base::string16>>({ |
| L"HKCU:32\\Software\\Some\\Unwanted Software", |
| L"HKCU:32\\Software\\Another\\Unwanted Software", |
| }); |
| break; |
| |
| default: |
| NOTREACHED(); |
| } |
| |
| extensions_reporting_ = extensions_reporting; |
| switch (extensions_reporting) { |
| case ItemsReporting::kUnsupported: |
| // Defined as an optional object in which an extensions vector is not |
| // present. |
| extension_ids_ = base::Optional<std::vector<base::string16>>(); |
| expected_extension_names_ = base::Optional<std::vector<base::string16>>(); |
| break; |
| |
| case ItemsReporting::kNotReported: |
| // Defined as an optional object in which an empty extensions vector is |
| // present. |
| extension_ids_ = |
| base::Optional<std::vector<base::string16>>(base::in_place); |
| expected_extension_names_ = |
| base::Optional<std::vector<base::string16>>(base::in_place); |
| break; |
| |
| case ItemsReporting::kReported: |
| // Defined as an optional object in which a non-empty extensions vector is |
| // present. |
| extension_ids_ = base::Optional<std::vector<base::string16>>({ |
| kInstalledExtensionId1, kInstalledExtensionId2, kUnknownExtensionId, |
| }); |
| // Scanner results only fetches extension names on Windows Chrome build. |
| #if defined(OS_WIN) && defined(GOOGLE_CHROME_BUILD) |
| expected_extension_names_ = base::Optional<std::vector<base::string16>>({ |
| kInstalledExtensionName1, kInstalledExtensionName2, |
| l10n_util::GetStringFUTF16( |
| IDS_SETTINGS_RESET_CLEANUP_DETAILS_EXTENSION_UNKNOWN, |
| kUnknownExtensionId), |
| }); |
| #else |
| expected_extension_names_ = |
| base::Optional<std::vector<base::string16>>(base::in_place); |
| #endif |
| break; |
| |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| int MockChromeCleanerProcess::Options::ExpectedExitCode( |
| PromptAcceptance received_prompt_acceptance) const { |
| if (crash_point() != CrashPoint::kNone) |
| return kDeliberateCrashExitCode; |
| |
| if (files_to_delete_.empty()) |
| return kNothingFoundExitCode; |
| |
| if (received_prompt_acceptance == PromptAcceptance::ACCEPTED_WITH_LOGS || |
| received_prompt_acceptance == PromptAcceptance::ACCEPTED_WITHOUT_LOGS) { |
| return reboot_required() ? kRebootRequiredExitCode |
| : kRebootNotRequiredExitCode; |
| } |
| |
| return kDeclinedExitCode; |
| } |
| |
| MockChromeCleanerProcess::MockChromeCleanerProcess( |
| const Options& options, |
| const std::string& chrome_mojo_pipe_token) |
| : options_(options), chrome_mojo_pipe_token_(chrome_mojo_pipe_token) {} |
| |
| int MockChromeCleanerProcess::Run() { |
| // We use EXPECT_*() macros to get good log lines, but since this code is run |
| // in a separate process, failing a check in an EXPECT_*() macro will not fail |
| // the test. Therefore, we use ::testing::Test::HasFailure() to detect |
| // EXPECT_*() failures and return an error code that indicates that the test |
| // should fail. |
| EXPECT_FALSE(chrome_mojo_pipe_token_.empty()); |
| if (::testing::Test::HasFailure()) |
| return kInternalTestFailureExitCode; |
| |
| if (options_.crash_point() == CrashPoint::kOnStartup) |
| exit(kDeliberateCrashExitCode); |
| |
| mojo::core::Init(); |
| base::Thread::Options thread_options(base::MessageLoop::TYPE_IO, 0); |
| base::Thread io_thread("IPCThread"); |
| EXPECT_TRUE(io_thread.StartWithOptions(thread_options)); |
| if (::testing::Test::HasFailure()) |
| return kInternalTestFailureExitCode; |
| |
| mojo::core::ScopedIPCSupport ipc_support( |
| io_thread.task_runner(), |
| mojo::core::ScopedIPCSupport::ShutdownPolicy::CLEAN); |
| |
| auto channel_endpoint = |
| mojo::PlatformChannel::RecoverPassedEndpointFromCommandLine( |
| *base::CommandLine::ForCurrentProcess()); |
| auto invitation = |
| mojo::IncomingInvitation::Accept(std::move(channel_endpoint)); |
| ChromePromptPtrInfo prompt_ptr_info( |
| invitation.ExtractMessagePipe(chrome_mojo_pipe_token_), 0); |
| |
| if (options_.crash_point() == CrashPoint::kAfterConnection) |
| exit(kDeliberateCrashExitCode); |
| |
| base::test::ScopedTaskEnvironment scoped_task_environment; |
| base::RunLoop run_loop; |
| // After the response from the parent process is received, this will post a |
| // task to unblock the child process's main thread. |
| auto quit_closure = base::BindOnce( |
| [](scoped_refptr<base::SequencedTaskRunner> main_runner, |
| base::Closure quit_closure) { |
| main_runner->PostTask(FROM_HERE, std::move(quit_closure)); |
| }, |
| base::SequencedTaskRunnerHandle::Get(), |
| base::Passed(run_loop.QuitClosure())); |
| |
| io_thread.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&MockChromeCleanerProcess::SendScanResults, |
| base::Unretained(this), std::move(prompt_ptr_info), |
| base::Passed(&quit_closure))); |
| |
| run_loop.Run(); |
| |
| EXPECT_NE(received_prompt_acceptance_, PromptAcceptance::UNSPECIFIED); |
| EXPECT_EQ(received_prompt_acceptance_, options_.expected_user_response()); |
| if (::testing::Test::HasFailure()) |
| return kInternalTestFailureExitCode; |
| return options_.ExpectedExitCode(received_prompt_acceptance_); |
| } |
| |
| void MockChromeCleanerProcess::SendScanResults( |
| ChromePromptPtrInfo prompt_ptr_info, |
| base::OnceClosure quit_closure) { |
| // This pointer will be deleted by PromptUserCallback. |
| chrome_prompt_ptr_ = new ChromePromptPtr(); |
| chrome_prompt_ptr_->Bind(std::move(prompt_ptr_info)); |
| |
| if (options_.crash_point() == CrashPoint::kAfterRequestSent) { |
| // This task is posted to the IPC thread so that it will happen after the |
| // request is sent to the parent process and before the response gets |
| // handled on the IPC thread. |
| base::SequencedTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::Bind([]() { exit(kDeliberateCrashExitCode); })); |
| } |
| |
| (*chrome_prompt_ptr_) |
| ->PromptUser( |
| options_.files_to_delete(), options_.registry_keys(), |
| options_.extension_ids(), |
| base::BindOnce(&MockChromeCleanerProcess::PromptUserCallback, |
| base::Unretained(this), std::move(quit_closure))); |
| } |
| |
| void MockChromeCleanerProcess::PromptUserCallback( |
| base::OnceClosure quit_closure, |
| PromptAcceptance prompt_acceptance) { |
| delete chrome_prompt_ptr_; |
| chrome_prompt_ptr_ = nullptr; |
| |
| received_prompt_acceptance_ = prompt_acceptance; |
| |
| if (options_.crash_point() == CrashPoint::kAfterResponseReceived) |
| exit(kDeliberateCrashExitCode); |
| |
| std::move(quit_closure).Run(); |
| } |
| |
| std::ostream& operator<<(std::ostream& out, CrashPoint crash_point) { |
| switch (crash_point) { |
| case CrashPoint::kNone: |
| return out << "NoCrash"; |
| case CrashPoint::kOnStartup: |
| return out << "CrashOnStartup"; |
| case CrashPoint::kAfterConnection: |
| return out << "CrashAfterConnection"; |
| case CrashPoint::kAfterRequestSent: |
| return out << "CrashAfterRequestSent"; |
| case CrashPoint::kAfterResponseReceived: |
| return out << "CrashAfterResponseReceived"; |
| default: |
| NOTREACHED(); |
| return out << "UnknownCrashPoint"; |
| } |
| } |
| |
| std::ostream& operator<<(std::ostream& out, UwsFoundStatus status) { |
| switch (status) { |
| case UwsFoundStatus::kNoUwsFound: |
| return out << "NoUwsFound"; |
| case UwsFoundStatus::kUwsFoundRebootRequired: |
| return out << "UwsFoundRebootRequired"; |
| case UwsFoundStatus::kUwsFoundNoRebootRequired: |
| return out << "UwsFoundNoRebootRequired"; |
| default: |
| NOTREACHED(); |
| return out << "UnknownFoundStatus"; |
| } |
| } |
| |
| std::ostream& operator<<(std::ostream& out, |
| ExtensionCleaningFeatureStatus status) { |
| switch (status) { |
| case ExtensionCleaningFeatureStatus::kEnabled: |
| return out << "ExtensionCleaningEnabled"; |
| case ExtensionCleaningFeatureStatus::kDisabled: |
| return out << "ExtensionCleaningDisabled"; |
| default: |
| NOTREACHED(); |
| return out << "UnknownExtensionCleaningStatus"; |
| } |
| } |
| |
| std::ostream& operator<<(std::ostream& out, ItemsReporting items_reporting) { |
| switch (items_reporting) { |
| case ItemsReporting::kUnsupported: |
| return out << "kUnsupported"; |
| case ItemsReporting::kNotReported: |
| return out << "kNotReported"; |
| case ItemsReporting::kReported: |
| return out << "kReported"; |
| default: |
| NOTREACHED(); |
| return out << "UnknownItemsReporting"; |
| } |
| } |
| |
| } // namespace safe_browsing |