| // 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/webui/settings/safety_check_extensions_handler.h" |
| |
| #include "chrome/browser/extensions/cws_info_service.h" |
| #include "chrome/browser/extensions/extension_management.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/grit/generated_resources.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_id.h" |
| #include "extensions/common/manifest.h" |
| |
| using extensions::ExtensionPrefs; |
| using extensions::ExtensionRegistry; |
| |
| namespace settings { |
| |
| namespace { |
| |
| // `kPrefAcknowledgeSafetyCheckWarning` should mirror the definition in |
| // chrome/browser/extensions/api/developer_private/developer_private_api.h. |
| constexpr extensions::PrefMap kPrefAcknowledgeSafetyCheckWarning = { |
| "ack_safety_check_warning", extensions::PrefType::kBool, |
| extensions::PrefScope::kExtensionSpecific}; |
| |
| } // namespace |
| |
| SafetyCheckExtensionsHandler::SafetyCheckExtensionsHandler(Profile* profile) |
| : profile_(profile) { |
| prefs_observation_.Observe(ExtensionPrefs::Get(profile_)); |
| extension_registry_observation_.Observe(ExtensionRegistry::Get(profile_)); |
| } |
| |
| SafetyCheckExtensionsHandler::~SafetyCheckExtensionsHandler() = default; |
| |
| void SafetyCheckExtensionsHandler::SetCWSInfoServiceForTest( |
| extensions::CWSInfoService* cws_info_service) { |
| cws_info_service_ = cws_info_service; |
| } |
| |
| void SafetyCheckExtensionsHandler::SetTriggeringExtensionsForTest( |
| extensions::ExtensionId extension_id) { |
| triggering_extensions_.insert(std::move(extension_id)); |
| } |
| |
| bool SafetyCheckExtensionsHandler::CheckExtensionForTrigger( |
| const extensions::Extension& extension) { |
| extensions::ExtensionPrefs* extension_prefs = |
| extensions::ExtensionPrefsFactory::GetForBrowserContext(profile_); |
| extensions::ExtensionManagement* extension_management = |
| extensions::ExtensionManagementFactory::GetForBrowserContext(profile_); |
| 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> cws_info = |
| cws_info_service_->GetCWSInfo(extension); |
| if (cws_info.has_value() && cws_info->is_present) { |
| switch (cws_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 (cws_info->unpublished_long_ago) { |
| return true; |
| } |
| break; |
| } |
| } |
| |
| // If an extension appears on the blocklist, that extension will be |
| // triggered. Currently, only malware and policy violation blocklist |
| // states are triggered. |
| 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)) { |
| if (extensions::Manifest::IsUnpackedLocation(extension.location())) { |
| bool dev_mode = |
| profile_->GetPrefs()->GetBoolean(prefs::kExtensionsUIDeveloperMode); |
| // Only show offstore extension in extension module if developer mode is |
| // disabled. |
| return !dev_mode; |
| } |
| |
| if (!extension_management->UpdatesFromWebstore(extension)) { |
| // extension does not update from the webstore. |
| return true; |
| } |
| |
| if (cws_info.has_value() && !cws_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; |
| } |
| |
| void SafetyCheckExtensionsHandler::HandleGetNumberOfExtensionsThatNeedReview( |
| const base::Value::List& args) { |
| const base::Value& callback_id = args[0]; |
| AllowJavascript(); |
| ResolveJavascriptCallback(callback_id, |
| base::Value(GetNumberOfExtensionsThatNeedReview())); |
| } |
| |
| int SafetyCheckExtensionsHandler::GetNumberOfExtensionsThatNeedReview() { |
| triggering_extensions_.clear(); |
| if (!base::FeatureList::IsEnabled(extensions::kCWSInfoService)) { |
| return 0; |
| } |
| |
| if (cws_info_service_ == nullptr) { |
| cws_info_service_ = extensions::CWSInfoService::Get(profile_); |
| } |
| |
| const extensions::ExtensionSet all_installed_extensions = |
| extensions::ExtensionRegistry::Get(profile_) |
| ->GenerateInstalledExtensionsSet(); |
| for (const auto& extension : all_installed_extensions) { |
| // Check if the extension is installed by a policy. |
| if (!extensions::Manifest::IsPolicyLocation(extension->location()) && |
| CheckExtensionForTrigger(*extension.get())) { |
| triggering_extensions_.insert(std::move(extension->id())); |
| } |
| } |
| return triggering_extensions_.size(); |
| } |
| |
| void SafetyCheckExtensionsHandler::UpdateNumberOfExtensionsThatNeedReview() { |
| AllowJavascript(); |
| int num_extensions = triggering_extensions_.size(); |
| FireWebUIListener("extensions-review-list-maybe-changed", num_extensions); |
| } |
| |
| void SafetyCheckExtensionsHandler::OnExtensionPrefsUpdated( |
| const std::string& extension_id) { |
| const extensions::Extension* extension = |
| extensions::ExtensionRegistry::Get(profile_)->GetExtensionById( |
| extension_id, extensions::ExtensionRegistry::EVERYTHING); |
| auto extension_ptr = triggering_extensions_.find(extension_id); |
| if (extension_ptr != triggering_extensions_.end() && |
| (!extension || !CheckExtensionForTrigger(*extension))) { |
| triggering_extensions_.erase(extension_ptr); |
| UpdateNumberOfExtensionsThatNeedReview(); |
| } |
| } |
| |
| void SafetyCheckExtensionsHandler::OnExtensionUninstalled( |
| content::BrowserContext* browser_context, |
| const extensions::Extension* extension, |
| extensions::UninstallReason reason) { |
| auto extension_ptr = triggering_extensions_.find(extension->id()); |
| if (extension_ptr != triggering_extensions_.end()) { |
| triggering_extensions_.erase(extension_ptr); |
| UpdateNumberOfExtensionsThatNeedReview(); |
| } |
| } |
| |
| void SafetyCheckExtensionsHandler::OnJavascriptAllowed() {} |
| |
| void SafetyCheckExtensionsHandler::OnJavascriptDisallowed() {} |
| |
| void SafetyCheckExtensionsHandler::RegisterMessages() { |
| // Usage of base::Unretained(this) is safe, because web_ui() owns `this` and |
| // won't release ownership until destruction. |
| web_ui()->RegisterMessageCallback( |
| "getNumberOfExtensionsThatNeedReview", |
| base::BindRepeating(&SafetyCheckExtensionsHandler:: |
| HandleGetNumberOfExtensionsThatNeedReview, |
| base::Unretained(this))); |
| } |
| |
| } // namespace settings |