blob: 2c20d56a867516b6b3a9f026cff956da03b6e19f [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/ui/safety_hub/extensions_result.h"
#include <memory>
#include <optional>
#include "base/notreached.h"
#include "base/ranges/algorithm.h"
#include "base/values.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/extensions/extension_management.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/safety_hub/safety_hub_constants.h"
#include "chrome/browser/ui/safety_hub/safety_hub_service.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/prefs/pref_service.h"
#include "extensions/browser/blocklist_extension_prefs.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_prefs_factory.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/extension_set.h"
#include "ui/base/l10n/l10n_util.h"
namespace {
constexpr extensions::PrefMap kPrefAcknowledgeSafetyCheckWarning = {
"ack_safety_check_warning", extensions::PrefType::kBool,
extensions::PrefScope::kExtensionSpecific};
// Return true if an extension should be reviewed in the Safety Hub
// Extension review panel. Any extensions that fall into one of the
// following categories will return true.
// -- Malware extensions
// -- Extensions with a CWS policy violation
// -- Extensions that have been unpublished
// -- Extensions marked as unwanted
// -- Offstore extensions
bool ShouldExtensionBeReviewed(
const extensions::Extension& extension,
Profile* profile,
const extensions::ExtensionPrefs* extension_prefs,
const extensions::CWSInfoService* extension_info_service,
bool consider_unpublished_only) {
bool warning_acked = false;
extension_prefs->ReadPrefAsBoolean(
extension.id(), kPrefAcknowledgeSafetyCheckWarning, &warning_acked);
bool is_extension = extension.is_extension() || extension.is_shared_module();
// If the user has previously acknowledged the warning on this
// extension and chosen to keep it, we will not show an additional
// Safety Hub warning. We also will not show warnings on Chrome apps.
if (warning_acked || !is_extension) {
return false;
}
std::optional<extensions::CWSInfoService::CWSInfo> extension_info =
extension_info_service->GetCWSInfo(extension);
// When only considering extensions that have been unpublished for a long
// time, discard any other review reason. Note that extensions can have
// multiple review reasons, and we're only returning the "main" one.
if (consider_unpublished_only) {
if (extension_info.has_value() && extension_info->unpublished_long_ago) {
return true;
}
return false;
}
if (extension_info.has_value() && extension_info->is_present) {
switch (extension_info->violation_type) {
case extensions::CWSInfoService::CWSViolationType::kMalware:
case extensions::CWSInfoService::CWSViolationType::kPolicy:
return true;
case extensions::CWSInfoService::CWSViolationType::kNone:
case extensions::CWSInfoService::CWSViolationType::kMinorPolicy:
case extensions::CWSInfoService::CWSViolationType::kUnknown:
if (extension_info->unpublished_long_ago) {
return true;
}
break;
}
}
// If an extension appears on the blocklist, that extension will be
// marked for review. Currently, only malware, policy violation, and
// potentially unwanted software blocklist states are marked for review.
extensions::BitMapBlocklistState blocklist_state =
extensions::blocklist_prefs::GetExtensionBlocklistState(extension.id(),
extension_prefs);
switch (blocklist_state) {
case extensions::BitMapBlocklistState::BLOCKLISTED_MALWARE:
case extensions::BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION:
return true;
case extensions::BitMapBlocklistState::BLOCKLISTED_POTENTIALLY_UNWANTED:
if (base::FeatureList::IsEnabled(
features::kSafetyHubExtensionsUwSTrigger)) {
return true;
}
break;
case extensions::BitMapBlocklistState::BLOCKLISTED_SECURITY_VULNERABILITY:
case extensions::BitMapBlocklistState::NOT_BLOCKLISTED:
// no-op.
break;
}
if (base::FeatureList::IsEnabled(
features::kSafetyHubExtensionsOffStoreTrigger)) {
// Check to see if the extension is an offstore extension.
if (extensions::Manifest::IsUnpackedLocation(extension.location())) {
// Only consider offstore extensions if developer mode is disabled.
bool dev_mode =
profile->GetPrefs()->GetBoolean(prefs::kExtensionsUIDeveloperMode);
return !dev_mode;
}
extensions::ExtensionManagement* extension_management =
extensions::ExtensionManagementFactory::GetForBrowserContext(profile);
if (!extension_management->UpdatesFromWebstore(extension)) {
// Extension does not update from the webstore.
return true;
}
if (extension_info.has_value() && !extension_info->is_present) {
// Handles the edge case where Chrome thinks that the extension is
// updating from the webstore but CWS has no knowledge of the extension.
return true;
}
}
return false;
}
} // namespace
SafetyHubExtensionsResult::SafetyHubExtensionsResult(
std::set<extensions::ExtensionId> triggering_extensions,
bool is_unpublished_extensions_only)
: triggering_extensions_(triggering_extensions),
is_unpublished_extensions_only_(is_unpublished_extensions_only) {}
SafetyHubExtensionsResult::SafetyHubExtensionsResult(
const SafetyHubExtensionsResult&) = default;
SafetyHubExtensionsResult& SafetyHubExtensionsResult::operator=(
const SafetyHubExtensionsResult&) = default;
SafetyHubExtensionsResult::~SafetyHubExtensionsResult() = default;
// static
std::optional<std::unique_ptr<SafetyHubService::Result>>
SafetyHubExtensionsResult::GetResult(
const extensions::CWSInfoService* extension_info_service,
Profile* profile,
bool only_unpublished_extensions = false) {
extensions::ExtensionPrefs* extension_prefs =
extensions::ExtensionPrefsFactory::GetForBrowserContext(profile);
extensions::ExtensionRegistry* extension_registry =
extensions::ExtensionRegistry::Get(profile);
std::set<extensions::ExtensionId> triggering_extensions;
if (base::FeatureList::IsEnabled(extensions::kCWSInfoService)) {
const extensions::ExtensionSet all_installed_extensions =
extension_registry->GenerateInstalledExtensionsSet();
for (const auto& extension : all_installed_extensions) {
// Check if the extension is installed by a policy.
if (extensions::Manifest::IsPolicyLocation(extension->location())) {
continue;
}
if (!ShouldExtensionBeReviewed(*extension.get(), profile, extension_prefs,
extension_info_service,
only_unpublished_extensions)) {
continue;
}
triggering_extensions.insert(std::move(extension->id()));
}
}
return std::make_unique<SafetyHubExtensionsResult>(
triggering_extensions, only_unpublished_extensions);
}
std::unique_ptr<SafetyHubService::Result> SafetyHubExtensionsResult::Clone()
const {
return std::make_unique<SafetyHubExtensionsResult>(*this);
}
base::Value::Dict SafetyHubExtensionsResult::ToDictValue() const {
// Only results that contain extensions that have been unpublished for a long
// time should be serialized.
CHECK(is_unpublished_extensions_only_);
base::Value::Dict result = BaseToDictValue();
base::Value::List extensions_list;
for (const auto& triggering_extension : triggering_extensions_) {
extensions_list.Append(triggering_extension);
}
result.Set(safety_hub::kSafetyHubTriggeringExtensionIdsKey,
std::move(extensions_list));
return result;
}
bool SafetyHubExtensionsResult::IsTriggerForMenuNotification() const {
// Only results that have unpublished extensions can result in a menu
// notification.
return is_unpublished_extensions_only_ && !triggering_extensions_.empty();
}
unsigned int SafetyHubExtensionsResult::GetNumTriggeringExtensions() const {
return triggering_extensions_.size();
}
bool SafetyHubExtensionsResult::WarrantsNewMenuNotification(
const base::Value::Dict& previous_result_dict) const {
std::set<extensions::ExtensionId> previous_triggering_extensions;
for (const base::Value& extension_id : *previous_result_dict.FindList(
safety_hub::kSafetyHubTriggeringExtensionIdsKey)) {
previous_triggering_extensions.insert(extension_id.GetString());
}
// Only results that are for unpublished extensions can result in a menu
// notification.
if (!is_unpublished_extensions_only_) {
return false;
}
return !base::ranges::includes(previous_triggering_extensions,
triggering_extensions_);
}
std::u16string SafetyHubExtensionsResult::GetNotificationString() const {
CHECK(is_unpublished_extensions_only_);
return l10n_util::GetPluralStringFUTF16(
IDS_SETTINGS_SAFETY_HUB_EXTENSIONS_MENU_NOTIFICATION,
triggering_extensions_.size());
}
int SafetyHubExtensionsResult::GetNotificationCommandId() const {
CHECK(is_unpublished_extensions_only_);
return IDC_MANAGE_EXTENSIONS;
}