| // Copyright 2024 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/extensions/extension_safety_check_utils.h" |
| |
| #include "chrome/browser/extensions/api/developer_private/developer_private_api.h" |
| #include "chrome/browser/extensions/cws_info_service.h" |
| #include "chrome/browser/extensions/extension_management.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/grit/branded_strings.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/prefs/pref_service.h" |
| #include "extensions/browser/blocklist_extension_prefs.h" |
| #include "extensions/browser/blocklist_state.h" |
| #include "extensions/browser/extension_prefs.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| namespace extensions { |
| |
| namespace developer = api::developer_private; |
| |
| namespace { |
| |
| // Update the old kPrefAcknowledgeSafetyCheckWarning pref to the new |
| // kPrefAcknowledgeSafetyCheckWarningReason pref. If only the boolean |
| // acknowledged pref is present, it's replaced with the new acknowledge |
| // reason pref set to the current top warning reason. If both the |
| // boolean and reason acknowledged pref are present, the bool pref is |
| // removed. |
| void MigrateSafetyCheckAcknowledgePref( |
| const Extension& extension, |
| developer::SafetyCheckWarningReason acknowledged_reason, |
| developer::SafetyCheckWarningReason top_warning_reason, |
| ExtensionPrefs* extension_prefs) { |
| bool extension_kept = false; |
| extension_prefs->ReadPrefAsBoolean( |
| extension.id(), extensions::kPrefAcknowledgeSafetyCheckWarning, |
| &extension_kept); |
| if (!extension_kept) { |
| return; |
| } |
| if (acknowledged_reason == developer::SafetyCheckWarningReason::kNone) { |
| extension_prefs->SetIntegerPref( |
| extension.id(), extensions::kPrefAcknowledgeSafetyCheckWarningReason, |
| static_cast<int>(top_warning_reason)); |
| } |
| // Remove the old boolean pref. |
| extension_prefs->UpdateExtensionPref( |
| extension.id(), extensions::kPrefAcknowledgeSafetyCheckWarning.name, |
| std::nullopt); |
| } |
| |
| // Returns true if the Safety Check should display a malware warning. |
| bool SafetyCheckShouldShowMalware( |
| BitMapBlocklistState blocklist_state, |
| const std::optional<CWSInfoService::CWSInfo>& cws_info) { |
| bool valid_cws_info = cws_info.has_value() && cws_info->is_present; |
| bool has_safe_browsing_malware_rating = |
| blocklist_state == BitMapBlocklistState::BLOCKLISTED_MALWARE; |
| bool has_cws_malware_rating = |
| valid_cws_info && |
| cws_info->violation_type == CWSInfoService::CWSViolationType::kMalware; |
| bool is_malware = has_safe_browsing_malware_rating || has_cws_malware_rating; |
| return is_malware; |
| } |
| |
| // Returns true if the Safety Check should display a policy violation warning. |
| bool SafetyCheckShouldShowPolicyViolation( |
| BitMapBlocklistState blocklist_state, |
| const std::optional<CWSInfoService::CWSInfo>& cws_info) { |
| bool valid_cws_info = cws_info.has_value() && cws_info->is_present; |
| bool has_safe_browsing_policy_rating = |
| blocklist_state == BitMapBlocklistState::BLOCKLISTED_CWS_POLICY_VIOLATION; |
| bool has_cws_policy_rating = |
| valid_cws_info && |
| cws_info->violation_type == CWSInfoService::CWSViolationType::kPolicy; |
| bool is_policy_violation = |
| has_safe_browsing_policy_rating || has_cws_policy_rating; |
| return is_policy_violation; |
| } |
| |
| // Returns true if the Safety Check should display an unwanted software warning. |
| bool SafetyCheckShouldShowPotentiallyUnwanted( |
| BitMapBlocklistState blocklist_state) { |
| bool is_potentially_unwanted = |
| blocklist_state == BitMapBlocklistState::BLOCKLISTED_POTENTIALLY_UNWANTED; |
| bool potentially_unwanted_enabled = |
| base::FeatureList::IsEnabled(features::kSafetyHubExtensionsUwSTrigger); |
| return potentially_unwanted_enabled && is_potentially_unwanted; |
| } |
| |
| // Returns true if the Safety Check should display a no privacy practice |
| // warning. |
| bool SafetyCheckShouldShowNoPrivacyPractice( |
| const std::optional<CWSInfoService::CWSInfo>& cws_info) { |
| bool valid_cws_info = cws_info.has_value() && cws_info->is_present; |
| bool no_privacy_practice_enabled = base::FeatureList::IsEnabled( |
| features::kSafetyHubExtensionsNoPrivacyPracticesTrigger); |
| bool has_no_privacy_practice_info = |
| valid_cws_info && cws_info->no_privacy_practice; |
| return no_privacy_practice_enabled && has_no_privacy_practice_info; |
| } |
| |
| // Returns true if the Safety Check should display a no off-store extension |
| // warning. |
| bool SafetyCheckShouldShowOffstoreExtension( |
| const Extension& extension, |
| Profile* profile, |
| const std::optional<CWSInfoService::CWSInfo>& cws_info) { |
| if (!base::FeatureList::IsEnabled( |
| features::kSafetyHubExtensionsOffStoreTrigger)) { |
| return false; |
| } |
| // Leave command-line extensions out for now since removing them is only |
| // effective till the next browser session. |
| if (extension.location() == mojom::ManifestLocation::kCommandLine) { |
| return false; |
| } |
| // Calculate if the extension triggers an off-store extension warning such as |
| // extensions that are no longer on the Chrome Web Store. |
| if (Manifest::IsUnpackedLocation(extension.location())) { |
| // Extensions that are unpacked will only trigger a review if dev |
| // mode is not enabled. |
| bool dev_mode = |
| profile->GetPrefs()->GetBoolean(prefs::kExtensionsUIDeveloperMode); |
| return !dev_mode; |
| } else { |
| ExtensionManagement* extension_management = |
| ExtensionManagementFactory::GetForBrowserContext(profile); |
| bool updates_from_webstore = |
| extension_management->UpdatesFromWebstore(extension); |
| if (updates_from_webstore) { |
| if (cws_info.has_value() && !cws_info->is_present) { |
| // If the extension has a webstore update URL but is not present |
| // in the webstore itself, then we will not consider it from |
| // the webstore. |
| return true; |
| } |
| } else { |
| // Extension does not update from the webstore. |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| // Return the `PrefAcknowledgeSafetyCheckWarningReason` pref as an enum. |
| developer::SafetyCheckWarningReason GetPrefAcknowledgeSafetyCheckWarningReason( |
| const Extension& extension, |
| ExtensionPrefs* extension_prefs) { |
| int kept_reason_int = 0; |
| extension_prefs->ReadPrefAsInteger( |
| extension.id(), extensions::kPrefAcknowledgeSafetyCheckWarningReason, |
| &kept_reason_int); |
| developer::SafetyCheckWarningReason acknowledged_reason = |
| static_cast<developer::SafetyCheckWarningReason>(kept_reason_int); |
| return (acknowledged_reason); |
| } |
| |
| // Converts the `SafetyCheckWarningReason` enum into its corresponding |
| // warning level. The greater the return value the more severe the |
| // trigger. |
| int GetSafetyCheckWarningLevel( |
| developer::SafetyCheckWarningReason safety_check_warning) { |
| switch (safety_check_warning) { |
| case developer::SafetyCheckWarningReason::kMalware: |
| return 6; |
| case developer::SafetyCheckWarningReason::kPolicy: |
| return 5; |
| case developer::SafetyCheckWarningReason::kUnwanted: |
| return 4; |
| case developer::SafetyCheckWarningReason::kUnpublished: |
| return 3; |
| case developer::SafetyCheckWarningReason::kNoPrivacyPractice: |
| return 2; |
| case developer::SafetyCheckWarningReason::kOffstore: |
| return 1; |
| case developer::SafetyCheckWarningReason::kNone: |
| return 0; |
| } |
| } |
| |
| // Checks if the user has already acknowledged a safety check warning that |
| // is of the same or greater level than the current warning reason. |
| // * current_warning - Current Safety Check warning reason |
| // * acknowledged_warning - Previously acknowledged warning reason |
| bool SafetyCheckHasUserAcknowledgedWarningLevel( |
| developer::SafetyCheckWarningReason acknowledged_reason, |
| developer::SafetyCheckWarningReason warning_reason) { |
| int acknowledged_reason_level = |
| GetSafetyCheckWarningLevel(acknowledged_reason); |
| int warning_reason_level = GetSafetyCheckWarningLevel(warning_reason); |
| return acknowledged_reason_level >= warning_reason_level; |
| } |
| |
| } // namespace |
| |
| namespace ExtensionSafetyCheckUtils { |
| |
| developer::SafetyCheckWarningReason GetSafetyCheckWarningReason( |
| const Extension& extension, |
| Profile* profile, |
| bool unpublished_only) { |
| CWSInfoService* cws_info_service = |
| CWSInfoService::Get(Profile::FromBrowserContext(profile)); |
| BitMapBlocklistState blocklist_state = |
| blocklist_prefs::GetExtensionBlocklistState(extension.id(), |
| ExtensionPrefs::Get(profile)); |
| return GetSafetyCheckWarningReasonHelper( |
| cws_info_service, blocklist_state, profile, extension, unpublished_only); |
| } |
| |
| developer::SafetyCheckWarningReason GetSafetyCheckWarningReasonHelper( |
| CWSInfoServiceInterface* cws_info_service, |
| BitMapBlocklistState blocklist_state, |
| Profile* profile, |
| const Extension& extension, |
| bool unpublished_only) { |
| developer::SafetyCheckWarningReason top_warning_reason = |
| developer::SafetyCheckWarningReason::kNone; |
| ExtensionManagement* extension_management = |
| ExtensionManagementFactory::GetForBrowserContext(profile); |
| bool is_extension = extension.is_extension() || extension.is_shared_module(); |
| bool is_non_visible_extension = |
| extensions::Manifest::IsComponentLocation(extension.location()); |
| bool is_explicitly_allowed_by_policy = |
| extension_management->IsInstallationExplicitlyAllowed(extension.id()); |
| // We do not show warnings for the following: |
| // - Chrome apps |
| // - Chrome extensions that are enterprise policy controlled OR not visible |
| // to the user. |
| if (!is_extension || is_non_visible_extension || |
| is_explicitly_allowed_by_policy) { |
| return developer::SafetyCheckWarningReason::kNone; |
| } |
| |
| developer::SafetyCheckWarningReason acknowledged_reason = |
| GetPrefAcknowledgeSafetyCheckWarningReason(extension, |
| ExtensionPrefs::Get(profile)); |
| std::optional<CWSInfoService::CWSInfo> cws_info = |
| cws_info_service->GetCWSInfo(extension); |
| bool valid_cws_info = cws_info.has_value() && cws_info->is_present; |
| if (unpublished_only) { |
| if (valid_cws_info && cws_info->unpublished_long_ago) { |
| top_warning_reason = developer::SafetyCheckWarningReason::kUnpublished; |
| } |
| } else { |
| if (SafetyCheckShouldShowMalware(blocklist_state, cws_info)) { |
| top_warning_reason = developer::SafetyCheckWarningReason::kMalware; |
| } else if (SafetyCheckShouldShowPolicyViolation(blocklist_state, |
| cws_info)) { |
| top_warning_reason = developer::SafetyCheckWarningReason::kPolicy; |
| } else if (SafetyCheckShouldShowPotentiallyUnwanted(blocklist_state)) { |
| top_warning_reason = developer::SafetyCheckWarningReason::kUnwanted; |
| } else if (valid_cws_info && cws_info->unpublished_long_ago) { |
| top_warning_reason = developer::SafetyCheckWarningReason::kUnpublished; |
| |
| } else if (SafetyCheckShouldShowNoPrivacyPractice(cws_info)) { |
| top_warning_reason = |
| developer::SafetyCheckWarningReason::kNoPrivacyPractice; |
| |
| } else if (SafetyCheckShouldShowOffstoreExtension(extension, profile, |
| cws_info)) { |
| top_warning_reason = developer::SafetyCheckWarningReason::kOffstore; |
| } |
| } |
| |
| // TODO(crbug.com/325469212) Remove after migration is deemed complete. |
| MigrateSafetyCheckAcknowledgePref(extension, acknowledged_reason, |
| top_warning_reason, |
| ExtensionPrefs::Get(profile)); |
| |
| // If user has chosen to keep the extension for the current, or a higher |
| // trigger reason, we will return no trigger. |
| if (SafetyCheckHasUserAcknowledgedWarningLevel(acknowledged_reason, |
| top_warning_reason)) { |
| return developer::SafetyCheckWarningReason::kNone; |
| } |
| return top_warning_reason; |
| } |
| |
| api::developer_private::SafetyCheckStrings GetSafetyCheckWarningStrings( |
| developer::SafetyCheckWarningReason warning_reason, |
| developer::ExtensionState state) { |
| developer::SafetyCheckStrings display_strings; |
| int detail_string_id = -1; |
| int panel_string_id = -1; |
| switch (warning_reason) { |
| case developer::SafetyCheckWarningReason::kMalware: |
| detail_string_id = IDS_SAFETY_CHECK_EXTENSIONS_MALWARE; |
| panel_string_id = IDS_EXTENSIONS_SC_MALWARE; |
| break; |
| case developer::SafetyCheckWarningReason::kPolicy: |
| detail_string_id = IDS_SAFETY_CHECK_EXTENSIONS_POLICY_VIOLATION; |
| panel_string_id = state == developer::ExtensionState::kEnabled |
| ? IDS_EXTENSIONS_SC_POLICY_VIOLATION_ON |
| : IDS_EXTENSIONS_SC_POLICY_VIOLATION_OFF; |
| break; |
| case developer::SafetyCheckWarningReason::kUnwanted: |
| detail_string_id = IDS_SAFETY_CHECK_EXTENSIONS_POLICY_VIOLATION; |
| panel_string_id = state == developer::ExtensionState::kEnabled |
| ? IDS_EXTENSIONS_SC_POLICY_VIOLATION_ON |
| : IDS_EXTENSIONS_SC_POLICY_VIOLATION_OFF; |
| break; |
| case developer::SafetyCheckWarningReason::kNoPrivacyPractice: |
| detail_string_id = IDS_EXTENSIONS_SAFETY_CHECK_NO_PRIVACY_PRACTICES; |
| panel_string_id = |
| state == developer::ExtensionState::kEnabled |
| ? IDS_EXTENSIONS_SAFETY_CHECK_NO_PRIVACY_PRACTICES_ON |
| : IDS_EXTENSIONS_SAFETY_CHECK_NO_PRIVACY_PRACTICES_OFF; |
| break; |
| case developer::SafetyCheckWarningReason::kOffstore: |
| detail_string_id = IDS_EXTENSIONS_SAFETY_CHECK_OFFSTORE; |
| panel_string_id = state == developer::ExtensionState::kEnabled |
| ? IDS_EXTENSIONS_SAFETY_CHECK_OFFSTORE_ON |
| : IDS_EXTENSIONS_SAFETY_CHECK_OFFSTORE_OFF; |
| break; |
| case developer::SafetyCheckWarningReason::kUnpublished: |
| detail_string_id = IDS_SAFETY_CHECK_EXTENSIONS_UNPUBLISHED; |
| panel_string_id = state == developer::ExtensionState::kEnabled |
| ? IDS_EXTENSIONS_SC_UNPUBLISHED_ON |
| : IDS_EXTENSIONS_SC_UNPUBLISHED_OFF; |
| break; |
| case developer::SafetyCheckWarningReason::kNone: |
| break; |
| } |
| if (detail_string_id != -1) { |
| display_strings.detail_string = l10n_util::GetStringUTF8(detail_string_id); |
| display_strings.panel_string = l10n_util::GetStringUTF8(panel_string_id); |
| } |
| return display_strings; |
| } |
| } // namespace ExtensionSafetyCheckUtils |
| } // namespace extensions |