chromium/components/safe_browsing/android/safe_browsing_api_handler_util.cc

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/safe_browsing/android/safe_browsing_api_handler_util.h"

#include <stddef.h>

#include <memory>
#include <string>

#include "base/json/json_reader.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/values.h"
#include "components/safe_browsing/core/browser/db/util.h"

namespace safe_browsing {
namespace {

// JSON metatdata keys. These are are fixed in the Java-side API.
const char kJsonKeyMatches[] = "matches";
const char kJsonKeyThreatType[] = "threat_type";

SubresourceFilterMatch ParseSubresourceFilterMatch(
    const base::Value::Dict& match) {
  SubresourceFilterMatch subresource_filter_match;

  auto get_enforcement = [](const std::string& value) {
    return value == "warn" ? SubresourceFilterLevel::WARN
                           : SubresourceFilterLevel::ENFORCE;
  };
  const std::string* absv_value = match.FindString("sf_absv");
  if (absv_value) {
    subresource_filter_match[SubresourceFilterType::ABUSIVE] =
        get_enforcement(*absv_value);
  }
  const std::string* bas_value = match.FindString("sf_bas");
  if (bas_value) {
    subresource_filter_match[SubresourceFilterType::BETTER_ADS] =
        get_enforcement(*bas_value);
  }
  return subresource_filter_match;
}

// Returns the severity level for a given SafeBrowsing list. The lowest value is
// 0, which represents the most severe list.
int GetThreatSeverity(SafetyNetJavaThreatType threat_type) {
  switch (threat_type) {
    case SafetyNetJavaThreatType::POTENTIALLY_HARMFUL_APPLICATION:
      return 0;
    case SafetyNetJavaThreatType::SOCIAL_ENGINEERING:
      return 1;
    case SafetyNetJavaThreatType::UNWANTED_SOFTWARE:
      return 2;
    case SafetyNetJavaThreatType::SUBRESOURCE_FILTER:
      return 3;
    case SafetyNetJavaThreatType::BILLING:
      return 4;
    case SafetyNetJavaThreatType::CSD_ALLOWLIST:
      return 5;
    case SafetyNetJavaThreatType::MAX_VALUE:
      return std::numeric_limits<int>::max();
  }
  NOTREACHED_IN_MIGRATION()
      << "Unhandled threat_type: " << static_cast<int>(threat_type);
  return std::numeric_limits<int>::max();
}

SBThreatType SafetyNetJavaToSBThreatType(
    SafetyNetJavaThreatType java_threat_num) {
  using enum SBThreatType;
  switch (java_threat_num) {
    case SafetyNetJavaThreatType::POTENTIALLY_HARMFUL_APPLICATION:
      return SB_THREAT_TYPE_URL_MALWARE;
    case SafetyNetJavaThreatType::UNWANTED_SOFTWARE:
      return SB_THREAT_TYPE_URL_UNWANTED;
    case SafetyNetJavaThreatType::SOCIAL_ENGINEERING:
      return SB_THREAT_TYPE_URL_PHISHING;
    case SafetyNetJavaThreatType::SUBRESOURCE_FILTER:
      return SB_THREAT_TYPE_SUBRESOURCE_FILTER;
    case SafetyNetJavaThreatType::BILLING:
      return SB_THREAT_TYPE_BILLING;
    default:
      // Unknown threat type
      return SB_THREAT_TYPE_SAFE;
  }
}

}  // namespace

// Valid examples:
// {"matches":[{"threat_type":"5"}]}
//   or
// {"matches":[{"threat_type":"4"},
//             {"threat_type":"5"}]}
UmaRemoteCallResult ParseJsonFromGMSCore(const std::string& metadata_str,
                                         SBThreatType* worst_sb_threat_type,
                                         ThreatMetadata* metadata) {
  *worst_sb_threat_type =
      SBThreatType::SB_THREAT_TYPE_SAFE;        // Default to safe.
  *metadata = ThreatMetadata();                 // Default values.

  if (metadata_str.empty())
    return UmaRemoteCallResult::JSON_EMPTY;

  // Pick out the "matches" list.
  std::optional<base::Value> value = base::JSONReader::Read(metadata_str);
  const base::Value::List* matches = nullptr;
  {
    if (!value.has_value())
      return UmaRemoteCallResult::JSON_FAILED_TO_PARSE;

    base::Value::Dict* dict = value->GetIfDict();
    if (!dict)
      return UmaRemoteCallResult::JSON_FAILED_TO_PARSE;

    matches = dict->FindList(kJsonKeyMatches);
    if (!matches)
      return UmaRemoteCallResult::JSON_FAILED_TO_PARSE;
  }

  // Go through each matched threat type and pick the most severe.
  SafetyNetJavaThreatType worst_threat_type =
      SafetyNetJavaThreatType::MAX_VALUE;
  const base::Value::Dict* worst_match = nullptr;
  for (const base::Value& match_value : *matches) {
    const base::Value::Dict* match = match_value.GetIfDict();
    if (!match) {
      continue;  // Skip malformed list entries.
    }

    // Get the threat number
    const std::string* threat_num_str = match->FindString(kJsonKeyThreatType);
    int threat_type_num;
    if (!threat_num_str ||
        !base::StringToInt(*threat_num_str, &threat_type_num)) {
      continue;  // Skip malformed list entries.
    }

    SafetyNetJavaThreatType threat_type =
        static_cast<SafetyNetJavaThreatType>(threat_type_num);
    if (threat_type > SafetyNetJavaThreatType::MAX_VALUE) {
      threat_type = SafetyNetJavaThreatType::MAX_VALUE;
    }
    if (GetThreatSeverity(threat_type) < GetThreatSeverity(worst_threat_type)) {
      worst_threat_type = threat_type;
      worst_match = match;
    }
  }

  *worst_sb_threat_type = SafetyNetJavaToSBThreatType(worst_threat_type);
  if (*worst_sb_threat_type == SBThreatType::SB_THREAT_TYPE_SAFE ||
      !worst_match) {
    return UmaRemoteCallResult::JSON_UNKNOWN_THREAT;
  }

  // Fill in the metadata
  if (*worst_sb_threat_type ==
      SBThreatType::SB_THREAT_TYPE_SUBRESOURCE_FILTER) {
    metadata->subresource_filter_match =
        ParseSubresourceFilterMatch(*worst_match);
  }

  return UmaRemoteCallResult::MATCH;  // success
}

ThreatMetadata GetThreatMetadataFromSafeBrowsingApi(
    SafeBrowsingJavaThreatType threat_type,
    const std::vector<int>& threat_attributes) {
  bool is_relevant_threat_type = false;
  SubresourceFilterType type;
  switch (threat_type) {
    case SafeBrowsingJavaThreatType::ABUSIVE_EXPERIENCE_VIOLATION:
      is_relevant_threat_type = true;
      type = SubresourceFilterType::ABUSIVE;
      break;
    case SafeBrowsingJavaThreatType::BETTER_ADS_VIOLATION:
      is_relevant_threat_type = true;
      type = SubresourceFilterType::BETTER_ADS;
      break;
    case SafeBrowsingJavaThreatType::NO_THREAT:
    case SafeBrowsingJavaThreatType::SOCIAL_ENGINEERING:
    case SafeBrowsingJavaThreatType::UNWANTED_SOFTWARE:
    case SafeBrowsingJavaThreatType::POTENTIALLY_HARMFUL_APPLICATION:
    case SafeBrowsingJavaThreatType::BILLING:
      break;
  }
  if (!is_relevant_threat_type) {
    return ThreatMetadata();
  }
  // Filter level is default to ENFORCE.
  SubresourceFilterLevel level = SubresourceFilterLevel::ENFORCE;
  for (int threat_attribute : threat_attributes) {
    if (threat_attribute ==
        static_cast<int>(SafeBrowsingJavaThreatAttribute::CANARY)) {
      level = SubresourceFilterLevel::WARN;
      break;
    }
  }

  SubresourceFilterMatch subresource_filter_match;
  subresource_filter_match[type] = level;
  ThreatMetadata threat_metadata;
  threat_metadata.subresource_filter_match = subresource_filter_match;
  return threat_metadata;
}

}  // namespace safe_browsing