blob: bf921c1c149523cd72b8e521986f60a5b29529c7 [file] [log] [blame]
// Copyright (c) 2019 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/cloud_content_scanning/deep_scanning_utils.h"
#include <algorithm>
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/extensions/api/safe_browsing_private/safe_browsing_private_event_router.h"
#include "chrome/browser/extensions/api/safe_browsing_private/safe_browsing_private_event_router_factory.h"
#include "chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "components/enterprise/common/proto/connectors.pb.h"
#include "components/signin/public/identity_manager/identity_manager.h"
namespace safe_browsing {
namespace {
constexpr int kMinBytesPerSecond = 1;
constexpr int kMaxBytesPerSecond = 100 * 1024 * 1024; // 100 MB/s
std::string MaybeGetUnscannedReason(BinaryUploadService::Result result) {
std::string unscanned_reason;
switch (result) {
case BinaryUploadService::Result::SUCCESS:
case BinaryUploadService::Result::UNAUTHORIZED:
// Don't report an unscanned file event on these results.
break;
case BinaryUploadService::Result::FILE_TOO_LARGE:
unscanned_reason = "FILE_TOO_LARGE";
break;
case BinaryUploadService::Result::TIMEOUT:
case BinaryUploadService::Result::UNKNOWN:
case BinaryUploadService::Result::UPLOAD_FAILURE:
case BinaryUploadService::Result::FAILED_TO_GET_TOKEN:
unscanned_reason = "SERVICE_UNAVAILABLE";
break;
case BinaryUploadService::Result::FILE_ENCRYPTED:
unscanned_reason = "FILE_PASSWORD_PROTECTED";
break;
case BinaryUploadService::Result::DLP_SCAN_UNSUPPORTED_FILE_TYPE:
unscanned_reason = "DLP_SCAN_UNSUPPORTED_FILE_TYPE";
}
return unscanned_reason;
}
} // namespace
ContentAnalysisTrigger::ContentAnalysisTrigger() = default;
ContentAnalysisTrigger::ContentAnalysisTrigger(
const ContentAnalysisTrigger& other) = default;
ContentAnalysisTrigger::ContentAnalysisTrigger(ContentAnalysisTrigger&& other) =
default;
ContentAnalysisTrigger::~ContentAnalysisTrigger() = default;
ContentAnalysisTrigger& ContentAnalysisTrigger::operator=(
const ContentAnalysisTrigger& other) = default;
ContentAnalysisScanResult::ContentAnalysisScanResult() = default;
ContentAnalysisScanResult::ContentAnalysisScanResult(
const ContentAnalysisScanResult& other) = default;
ContentAnalysisScanResult::ContentAnalysisScanResult(
ContentAnalysisScanResult&& other) = default;
ContentAnalysisScanResult::~ContentAnalysisScanResult() = default;
ContentAnalysisScanResult& ContentAnalysisScanResult::operator=(
const ContentAnalysisScanResult& other) = default;
void MaybeReportDeepScanningVerdict(
Profile* profile,
const GURL& url,
const std::string& file_name,
const std::string& download_digest_sha256,
const std::string& mime_type,
const std::string& trigger,
DeepScanAccessPoint access_point,
const int64_t content_size,
BinaryUploadService::Result result,
const DeepScanningClientResponse& response) {
DCHECK(std::all_of(download_digest_sha256.begin(),
download_digest_sha256.end(), [](const char& c) {
return (c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}));
std::string unscanned_reason = MaybeGetUnscannedReason(result);
if (!unscanned_reason.empty()) {
extensions::SafeBrowsingPrivateEventRouterFactory::GetForProfile(profile)
->OnUnscannedFileEvent(url, file_name, download_digest_sha256,
mime_type, trigger, access_point,
unscanned_reason, content_size);
}
if (result != BinaryUploadService::Result::SUCCESS)
return;
if (response.has_malware_scan_verdict() &&
response.malware_scan_verdict().verdict() ==
MalwareDeepScanningVerdict::SCAN_FAILURE) {
extensions::SafeBrowsingPrivateEventRouterFactory::GetForProfile(profile)
->OnUnscannedFileEvent(url, file_name, download_digest_sha256,
mime_type, trigger, access_point,
"MALWARE_SCAN_FAILED", content_size);
}
if (response.has_dlp_scan_verdict() &&
response.dlp_scan_verdict().status() != DlpDeepScanningVerdict::SUCCESS) {
extensions::SafeBrowsingPrivateEventRouterFactory::GetForProfile(profile)
->OnUnscannedFileEvent(url, file_name, download_digest_sha256,
mime_type, trigger, access_point,
"DLP_SCAN_FAILED", content_size);
}
if (response.malware_scan_verdict().verdict() ==
MalwareDeepScanningVerdict::UWS ||
response.malware_scan_verdict().verdict() ==
MalwareDeepScanningVerdict::MALWARE) {
extensions::SafeBrowsingPrivateEventRouterFactory::GetForProfile(profile)
->OnAnalysisConnectorResult(
url, file_name, download_digest_sha256, mime_type, trigger,
access_point,
MalwareVerdictToResult(response.malware_scan_verdict()),
content_size);
}
if (response.dlp_scan_verdict().status() == DlpDeepScanningVerdict::SUCCESS) {
if (!response.dlp_scan_verdict().triggered_rules().empty()) {
extensions::SafeBrowsingPrivateEventRouterFactory::GetForProfile(profile)
->OnAnalysisConnectorResult(
url, file_name, download_digest_sha256, mime_type, trigger,
access_point,
SensitiveDataVerdictToResult(response.dlp_scan_verdict()),
content_size);
}
}
}
void MaybeReportDeepScanningVerdict(
Profile* profile,
const GURL& url,
const std::string& file_name,
const std::string& download_digest_sha256,
const std::string& mime_type,
const std::string& trigger,
DeepScanAccessPoint access_point,
const int64_t content_size,
BinaryUploadService::Result result,
const enterprise_connectors::ContentAnalysisResponse& response) {
DCHECK(std::all_of(download_digest_sha256.begin(),
download_digest_sha256.end(), [](const char& c) {
return (c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}));
std::string unscanned_reason = MaybeGetUnscannedReason(result);
if (!unscanned_reason.empty()) {
extensions::SafeBrowsingPrivateEventRouterFactory::GetForProfile(profile)
->OnUnscannedFileEvent(url, file_name, download_digest_sha256,
mime_type, trigger, access_point,
unscanned_reason, content_size);
}
if (result != BinaryUploadService::Result::SUCCESS)
return;
for (auto result : response.results()) {
if (result.status() !=
enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS) {
extensions::SafeBrowsingPrivateEventRouterFactory::GetForProfile(profile)
->OnUnscannedFileEvent(url, file_name, download_digest_sha256,
mime_type, trigger, access_point,
"ANALYSIS_CONNECTOR_FAILED", content_size);
} else if (result.triggered_rules_size() > 0) {
extensions::SafeBrowsingPrivateEventRouterFactory::GetForProfile(profile)
->OnAnalysisConnectorResult(url, file_name, download_digest_sha256,
mime_type, trigger, access_point,
ContentAnalysisResultToResult(result),
content_size);
}
}
}
void ReportAnalysisConnectorWarningBypass(
Profile* profile,
const GURL& url,
const std::string& file_name,
const std::string& download_digest_sha256,
const std::string& mime_type,
const std::string& trigger,
DeepScanAccessPoint access_point,
const int64_t content_size,
const DlpDeepScanningVerdict& verdict) {
DCHECK(std::all_of(download_digest_sha256.begin(),
download_digest_sha256.end(), [](const char& c) {
return (c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}));
extensions::SafeBrowsingPrivateEventRouterFactory::GetForProfile(profile)
->OnAnalysisConnectorWarningBypassed(
url, file_name, download_digest_sha256, mime_type, trigger,
access_point, SensitiveDataVerdictToResult(verdict), content_size);
}
void ReportAnalysisConnectorWarningBypass(
Profile* profile,
const GURL& url,
const std::string& file_name,
const std::string& download_digest_sha256,
const std::string& mime_type,
const std::string& trigger,
DeepScanAccessPoint access_point,
const int64_t content_size,
const enterprise_connectors::ContentAnalysisResponse& response) {
DCHECK(std::all_of(download_digest_sha256.begin(),
download_digest_sha256.end(), [](const char& c) {
return (c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}));
auto results = ContentAnalysisResponseToResults(response);
for (auto result : results) {
extensions::SafeBrowsingPrivateEventRouterFactory::GetForProfile(profile)
->OnAnalysisConnectorWarningBypassed(
url, file_name, download_digest_sha256, mime_type, trigger,
access_point, result, content_size);
}
}
std::string DeepScanAccessPointToString(DeepScanAccessPoint access_point) {
switch (access_point) {
case DeepScanAccessPoint::DOWNLOAD:
return "Download";
case DeepScanAccessPoint::UPLOAD:
return "Upload";
case DeepScanAccessPoint::DRAG_AND_DROP:
return "DragAndDrop";
case DeepScanAccessPoint::PASTE:
return "Paste";
}
NOTREACHED();
return "";
}
void RecordDeepScanMetrics(
DeepScanAccessPoint access_point,
base::TimeDelta duration,
int64_t total_bytes,
const BinaryUploadService::Result& result,
const enterprise_connectors::ContentAnalysisResponse& response) {
// Don't record UMA metrics for this result.
if (result == BinaryUploadService::Result::UNAUTHORIZED)
return;
bool dlp_verdict_success = true;
bool malware_verdict_success = true;
for (const auto& result : response.results()) {
if (result.tag() == "dlp" &&
result.status() !=
enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS) {
dlp_verdict_success = false;
}
if (result.tag() == "malware" &&
result.status() !=
enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS) {
malware_verdict_success = false;
}
}
bool success = dlp_verdict_success && malware_verdict_success;
std::string result_value = BinaryUploadServiceResultToString(result, success);
// Update |success| so non-SUCCESS results don't log the bytes/sec metric.
success &= (result == BinaryUploadService::Result::SUCCESS);
RecordDeepScanMetrics(access_point, duration, total_bytes, result_value,
success);
}
void RecordDeepScanMetrics(DeepScanAccessPoint access_point,
base::TimeDelta duration,
int64_t total_bytes,
const BinaryUploadService::Result& result,
const DeepScanningClientResponse& response) {
// Don't record UMA metrics for this result.
if (result == BinaryUploadService::Result::UNAUTHORIZED)
return;
bool dlp_verdict_success = response.has_dlp_scan_verdict()
? response.dlp_scan_verdict().status() ==
DlpDeepScanningVerdict::SUCCESS
: true;
bool malware_verdict_success = true;
if (response.has_malware_scan_verdict()) {
switch (response.malware_scan_verdict().verdict()) {
case MalwareDeepScanningVerdict::VERDICT_UNSPECIFIED:
case MalwareDeepScanningVerdict::SCAN_FAILURE:
malware_verdict_success = false;
break;
case MalwareDeepScanningVerdict::MALWARE:
case MalwareDeepScanningVerdict::UWS:
case MalwareDeepScanningVerdict::CLEAN:
malware_verdict_success = true;
break;
}
}
bool success = dlp_verdict_success && malware_verdict_success;
std::string result_value = BinaryUploadServiceResultToString(result, success);
// Update |success| so non-SUCCESS results don't log the bytes/sec metric.
success &= (result == BinaryUploadService::Result::SUCCESS);
RecordDeepScanMetrics(access_point, duration, total_bytes, result_value,
success);
}
void RecordDeepScanMetrics(DeepScanAccessPoint access_point,
base::TimeDelta duration,
int64_t total_bytes,
const std::string& result,
bool success) {
// Don't record metrics if the duration is unusable.
if (duration.InMilliseconds() == 0)
return;
std::string access_point_string = DeepScanAccessPointToString(access_point);
if (success) {
base::UmaHistogramCustomCounts(
"SafeBrowsing.DeepScan." + access_point_string + ".BytesPerSeconds",
(1000 * total_bytes) / duration.InMilliseconds(),
/*min=*/kMinBytesPerSecond,
/*max=*/kMaxBytesPerSecond,
/*buckets=*/50);
}
// The scanning timeout is 5 minutes, so the bucket maximum time is 30 minutes
// in order to be lenient and avoid having lots of data in the overlow bucket.
base::UmaHistogramCustomTimes("SafeBrowsing.DeepScan." + access_point_string +
"." + result + ".Duration",
duration, base::TimeDelta::FromMilliseconds(1),
base::TimeDelta::FromMinutes(30), 50);
base::UmaHistogramCustomTimes(
"SafeBrowsing.DeepScan." + access_point_string + ".Duration", duration,
base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromMinutes(30),
50);
}
std::array<const base::FilePath::CharType*, 24> SupportedDlpFileTypes() {
// Keep sorted for efficient access.
static constexpr const std::array<const base::FilePath::CharType*, 24>
kSupportedDLPFileTypes = {
FILE_PATH_LITERAL(".7z"), FILE_PATH_LITERAL(".bz2"),
FILE_PATH_LITERAL(".bzip"), FILE_PATH_LITERAL(".cab"),
FILE_PATH_LITERAL(".csv"), FILE_PATH_LITERAL(".doc"),
FILE_PATH_LITERAL(".docx"), FILE_PATH_LITERAL(".eps"),
FILE_PATH_LITERAL(".gz"), FILE_PATH_LITERAL(".gzip"),
FILE_PATH_LITERAL(".odt"), FILE_PATH_LITERAL(".pdf"),
FILE_PATH_LITERAL(".ppt"), FILE_PATH_LITERAL(".pptx"),
FILE_PATH_LITERAL(".ps"), FILE_PATH_LITERAL(".rar"),
FILE_PATH_LITERAL(".rtf"), FILE_PATH_LITERAL(".tar"),
FILE_PATH_LITERAL(".txt"), FILE_PATH_LITERAL(".wpd"),
FILE_PATH_LITERAL(".xls"), FILE_PATH_LITERAL(".xlsx"),
FILE_PATH_LITERAL(".xps"), FILE_PATH_LITERAL(".zip")};
// TODO: Replace this DCHECK with a static assert once std::is_sorted is
// constexpr in C++20.
DCHECK(std::is_sorted(
kSupportedDLPFileTypes.begin(), kSupportedDLPFileTypes.end(),
[](const base::FilePath::StringType& a,
const base::FilePath::StringType& b) { return a.compare(b) < 0; }));
return kSupportedDLPFileTypes;
}
bool FileTypeSupportedForDlp(const base::FilePath& path) {
// Accept any file type in the supported list for DLP scans.
base::FilePath::StringType extension(path.FinalExtension());
std::transform(extension.begin(), extension.end(), extension.begin(),
tolower);
auto dlp_types = SupportedDlpFileTypes();
return std::binary_search(dlp_types.begin(), dlp_types.end(), extension);
}
DeepScanningClientResponse SimpleDeepScanningClientResponseForTesting(
base::Optional<bool> dlp_success,
base::Optional<bool> malware_success) {
DeepScanningClientResponse response;
if (dlp_success.has_value()) {
response.mutable_dlp_scan_verdict()->set_status(
DlpDeepScanningVerdict::SUCCESS);
if (!dlp_success.value()) {
DlpDeepScanningVerdict::TriggeredRule* rule =
response.mutable_dlp_scan_verdict()->add_triggered_rules();
rule->set_rule_name("rule");
rule->set_action(DlpDeepScanningVerdict::TriggeredRule::BLOCK);
}
}
if (malware_success.has_value()) {
if (malware_success.value()) {
response.mutable_malware_scan_verdict()->set_verdict(
MalwareDeepScanningVerdict::CLEAN);
} else {
response.mutable_malware_scan_verdict()->set_verdict(
MalwareDeepScanningVerdict::MALWARE);
}
}
return response;
}
std::string BinaryUploadServiceResultToString(
const BinaryUploadService::Result& result,
bool success) {
switch (result) {
case BinaryUploadService::Result::SUCCESS:
if (success)
return "Success";
else
return "FailedToGetVerdict";
case BinaryUploadService::Result::UPLOAD_FAILURE:
return "UploadFailure";
case BinaryUploadService::Result::TIMEOUT:
return "Timeout";
case BinaryUploadService::Result::FILE_TOO_LARGE:
return "FileTooLarge";
case BinaryUploadService::Result::FAILED_TO_GET_TOKEN:
return "FailedToGetToken";
case BinaryUploadService::Result::UNKNOWN:
return "Unknown";
case BinaryUploadService::Result::UNAUTHORIZED:
return "";
case BinaryUploadService::Result::FILE_ENCRYPTED:
return "FileEncrypted";
case BinaryUploadService::Result::DLP_SCAN_UNSUPPORTED_FILE_TYPE:
return "DlpScanUnsupportedFileType";
}
}
ContentAnalysisScanResult SensitiveDataVerdictToResult(
const safe_browsing::DlpDeepScanningVerdict& verdict) {
ContentAnalysisScanResult result;
result.tag = "dlp";
result.status = verdict.status();
for (auto rule : verdict.triggered_rules()) {
ContentAnalysisTrigger trigger;
trigger.action = rule.action();
trigger.id =
rule.has_rule_id() ? base::NumberToString(rule.rule_id()) : "0";
trigger.name = rule.rule_name();
result.triggers.push_back(std::move(trigger));
}
return result;
}
ContentAnalysisScanResult ContentAnalysisResultToResult(
const enterprise_connectors::ContentAnalysisResponse::Result& result) {
ContentAnalysisScanResult result2;
result2.tag = result.tag();
result2.status = result.status();
for (auto rule : result.triggered_rules()) {
ContentAnalysisTrigger trigger;
trigger.action = rule.action();
trigger.id = rule.rule_id();
trigger.name = rule.rule_name();
result2.triggers.push_back(std::move(trigger));
}
return result2;
}
ContentAnalysisScanResult MalwareVerdictToResult(
const safe_browsing::MalwareDeepScanningVerdict& verdict) {
DCHECK_NE(MalwareDeepScanningVerdict::VERDICT_UNSPECIFIED, verdict.verdict());
DCHECK_NE(MalwareDeepScanningVerdict::SCAN_FAILURE, verdict.verdict());
ContentAnalysisScanResult result;
result.tag = "malware";
result.status =
enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS;
if (verdict.verdict() != MalwareDeepScanningVerdict::CLEAN) {
ContentAnalysisTrigger trigger;
switch (verdict.verdict()) {
case MalwareDeepScanningVerdict::UWS:
trigger.action = enterprise_connectors::ContentAnalysisResponse::
Result::TriggeredRule::BLOCK;
trigger.name = "UWS";
break;
case MalwareDeepScanningVerdict::MALWARE:
trigger.action = enterprise_connectors::ContentAnalysisResponse::
Result::TriggeredRule::BLOCK;
trigger.name = "MALWARE";
break;
default:
NOTREACHED();
break;
}
result.triggers.push_back(std::move(trigger));
}
return result;
}
std::vector<ContentAnalysisScanResult> ContentAnalysisResponseToResults(
const enterprise_connectors::ContentAnalysisResponse& response) {
std::vector<ContentAnalysisScanResult> results;
for (auto result : response.results()) {
results.push_back(ContentAnalysisResultToResult(result));
}
return results;
}
std::string GetProfileEmail(Profile* profile) {
return profile
? GetProfileEmail(IdentityManagerFactory::GetForProfile(profile))
: std::string();
}
std::string GetProfileEmail(signin::IdentityManager* identity_manager) {
// If the profile is not signed in, GetPrimaryAccountInfo() returns an
// empty account info.
return identity_manager
? identity_manager
->GetPrimaryAccountInfo(signin::ConsentLevel::kNotRequired)
.email
: std::string();
}
} // namespace safe_browsing