blob: 638fa057f1a5f675ed5b1c4276f21a411fa0caef [file] [log] [blame]
// 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),
&registry_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::BindOnce([]() { 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