blob: fa6e9209c216169cd1c7ba50fb82de7233669a84 [file] [log] [blame]
// Copyright 2022 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/enterprise/connectors/analysis/files_request_handler.h"
#include <algorithm>
#include "base/check_op.h"
#include "base/files/file_path.h"
#include "base/files/scoped_file.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "chrome/browser/enterprise/connectors/common.h"
#include "chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.h"
#include "chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_utils.h"
#include "chrome/browser/safe_browsing/cloud_content_scanning/file_opening_job.h"
#include "components/enterprise/common/proto/connectors.pb.h"
#include "components/enterprise/connectors/core/reporting_constants.h"
#include "components/file_access/scoped_file_access.h"
#include "components/file_access/scoped_file_access_delegate.h"
#include "components/safe_browsing/content/browser/web_ui/safe_browsing_ui.h"
namespace enterprise_connectors {
namespace {
constexpr char kFileAttachCount[] = "Enterprise.OnFileAttach.FileCount";
constexpr char kFileTransferCount[] = "Enterprise.OnFileTransfer.FileCount";
// Global pointer of factory function (RepeatingCallback) used to create
// instances of ContentAnalysisDelegate in tests. !is_null() only in tests.
FilesRequestHandler::Factory* GetFactoryStorage() {
static base::NoDestructor<FilesRequestHandler::Factory> factory;
return factory.get();
}
AnalysisConnector AccessPointToEnterpriseConnector(
DeepScanAccessPoint access_point) {
switch (access_point) {
case DeepScanAccessPoint::FILE_TRANSFER:
return enterprise_connectors::FILE_TRANSFER;
case DeepScanAccessPoint::UPLOAD:
case DeepScanAccessPoint::DRAG_AND_DROP:
case DeepScanAccessPoint::PASTE:
// A file can be uploaded to a website by either a normal file picker, a
// dragNdrop event or using copy+paste.
return enterprise_connectors::FILE_ATTACHED;
case DeepScanAccessPoint::DOWNLOAD:
case DeepScanAccessPoint::PRINT:
NOTREACHED();
}
return enterprise_connectors::FILE_ATTACHED;
}
std::string AccessPointToTriggerString(DeepScanAccessPoint access_point) {
switch (access_point) {
case DeepScanAccessPoint::FILE_TRANSFER:
return kFileTransferDataTransferEventTrigger;
case DeepScanAccessPoint::UPLOAD:
case DeepScanAccessPoint::DRAG_AND_DROP:
case DeepScanAccessPoint::PASTE:
// A file can be uploaded to a website by either a normal file picker, a
// dragNdrop event or using copy+paste.
return kFileUploadDataTransferEventTrigger;
case DeepScanAccessPoint::DOWNLOAD:
case DeepScanAccessPoint::PRINT:
NOTREACHED();
}
return "";
}
} // namespace
FilesRequestHandler::FileInfo::FileInfo() = default;
FilesRequestHandler::FileInfo::FileInfo(FileInfo&& other) = default;
FilesRequestHandler::FileInfo::~FileInfo() = default;
FilesRequestHandler::FilesRequestHandler(
ContentAnalysisInfo* content_analysis_info,
safe_browsing::BinaryUploadService* upload_service,
Profile* profile,
GURL url,
const std::string& source,
const std::string& destination,
const std::string& content_transfer_method,
DeepScanAccessPoint access_point,
const std::vector<base::FilePath>& paths,
CompletionCallback callback)
: RequestHandlerBase(content_analysis_info,
upload_service,
profile,
url,
access_point),
paths_(paths),
source_(source),
destination_(destination),
content_transfer_method_(content_transfer_method),
callback_(std::move(callback)) {
results_.resize(paths_.size());
file_info_.resize(paths_.size());
start_times_.resize(paths_.size(), base::TimeTicks::Min());
}
// static
std::unique_ptr<FilesRequestHandler> FilesRequestHandler::Create(
ContentAnalysisInfo* content_analysis_info,
safe_browsing::BinaryUploadService* upload_service,
Profile* profile,
GURL url,
const std::string& source,
const std::string& destination,
const std::string& content_transfer_method,
DeepScanAccessPoint access_point,
const std::vector<base::FilePath>& paths,
CompletionCallback callback) {
if (GetFactoryStorage()->is_null()) {
return base::WrapUnique(new FilesRequestHandler(
content_analysis_info, upload_service, profile, url, source,
destination, content_transfer_method, access_point, paths,
std::move(callback)));
} else {
// Use the factory to create a fake FilesRequestHandler.
return GetFactoryStorage()->Run(content_analysis_info, upload_service,
profile, url, source, destination,
content_transfer_method, access_point,
paths, std::move(callback));
}
}
// static
void FilesRequestHandler::SetFactoryForTesting(Factory factory) {
*GetFactoryStorage() = factory;
}
// static
void FilesRequestHandler::ResetFactoryForTesting() {
if (GetFactoryStorage())
GetFactoryStorage()->Reset();
}
FilesRequestHandler::~FilesRequestHandler() = default;
void FilesRequestHandler::ReportWarningBypass(
std::optional<std::u16string> user_justification) {
// Report a warning bypass for each previously warned file.
for (const auto& warning : file_warnings_) {
size_t index = warning.first;
ReportAnalysisConnectorWarningBypass(
profile_, url_, url_, source_, destination_,
paths_[index].AsUTF8Unsafe(), file_info_[index].sha256,
file_info_[index].mime_type, AccessPointToTriggerString(access_point_),
content_transfer_method_, file_info_[index].size,
content_analysis_info_->referrer_chain(), warning.second,
user_justification);
}
}
void FilesRequestHandler::FileRequestCallbackForTesting(
base::FilePath path,
safe_browsing::BinaryUploadService::Result result,
enterprise_connectors::ContentAnalysisResponse response) {
auto it = std::ranges::find(paths_, path);
CHECK(it != paths_.end());
size_t index = std::distance(paths_.begin(), it);
FileRequestCallback(index, result, response);
}
bool FilesRequestHandler::UploadDataImpl() {
safe_browsing::IncrementCrashKey(
safe_browsing::ScanningCrashKey::PENDING_FILE_UPLOADS, paths_.size());
if (!paths_.empty()) {
safe_browsing::IncrementCrashKey(
safe_browsing::ScanningCrashKey::TOTAL_FILE_UPLOADS, paths_.size());
std::vector<safe_browsing::FileOpeningJob::FileOpeningTask> tasks(
paths_.size());
for (size_t i = 0; i < paths_.size(); ++i)
tasks[i].request = PrepareFileRequest(i);
file_access::RequestFilesAccessForSystem(
paths_,
base::BindOnce(&FilesRequestHandler::CreateFileOpeningJob,
weak_ptr_factory_.GetWeakPtr(), std::move(tasks)));
switch (AccessPointToEnterpriseConnector(access_point_)) {
case enterprise_connectors::FILE_ATTACHED:
base::UmaHistogramCustomCounts(kFileAttachCount, paths_.size(), 1, 1000,
100);
break;
case enterprise_connectors::FILE_TRANSFER:
base::UmaHistogramCustomCounts(kFileTransferCount, paths_.size(), 1,
1000, 100);
break;
default:
break;
}
return true;
}
// If zero files were passed to the FilesRequestHandler, we call the callback
// directly.
MaybeCompleteScanRequest();
return false;
}
safe_browsing::FileAnalysisRequest* FilesRequestHandler::PrepareFileRequest(
size_t index) {
DCHECK_LT(index, paths_.size());
base::FilePath path = paths_[index];
auto request = std::make_unique<safe_browsing::FileAnalysisRequest>(
content_analysis_info_->settings(), path, path.BaseName(),
/*mime_type*/ "",
/* delay_opening_file */ true,
base::BindOnce(&FilesRequestHandler::FileRequestCallback,
weak_ptr_factory_.GetWeakPtr(), index),
base::BindOnce(&FilesRequestHandler::FileRequestStartCallback,
weak_ptr_factory_.GetWeakPtr(), index));
safe_browsing::FileAnalysisRequest* request_raw = request.get();
content_analysis_info_->InitializeRequest(request_raw);
request_raw->set_analysis_connector(
AccessPointToEnterpriseConnector(access_point_));
request_raw->set_source(source_);
request_raw->set_destination(destination_);
request_raw->GetRequestData(base::BindOnce(
&FilesRequestHandler::OnGotFileInfo, weak_ptr_factory_.GetWeakPtr(),
std::move(request), index));
return request_raw;
}
void FilesRequestHandler::OnGotFileInfo(
std::unique_ptr<safe_browsing::BinaryUploadService::Request> request,
size_t index,
safe_browsing::BinaryUploadService::Result result,
safe_browsing::BinaryUploadService::Request::Data data) {
DCHECK_LT(index, paths_.size());
DCHECK_EQ(paths_.size(), file_info_.size());
file_info_[index].sha256 = data.hash;
file_info_[index].size = data.size;
file_info_[index].mime_type = data.mime_type;
const auto& analysis_settings = content_analysis_info_->settings();
bool is_cloud = analysis_settings.cloud_or_local_settings.is_cloud_analysis();
bool is_resumable = IsResumableUpload(*request);
bool failed = is_resumable
? CloudResumableResultIsFailure(
result, analysis_settings.block_large_files,
analysis_settings.block_password_protected_files)
: (is_cloud ? CloudMultipartResultIsFailure(result)
: LocalResultIsFailure(result));
if (failed) {
FinishRequestEarly(std::move(request), result);
return;
}
// Don't bother sending empty files for deep scanning.
if (data.size == 0) {
FinishRequestEarly(std::move(request),
safe_browsing::BinaryUploadService::Result::SUCCESS);
return;
}
// If |throttled_| is true, then the file shouldn't be upload since the server
// is receiving too many requests.
if (throttled_) {
FinishRequestEarly(
std::move(request),
safe_browsing::BinaryUploadService::Result::TOO_MANY_REQUESTS);
return;
}
UploadFileForDeepScanning(result, paths_[index], std::move(request));
}
void FilesRequestHandler::FinishRequestEarly(
std::unique_ptr<safe_browsing::BinaryUploadService::Request> request,
safe_browsing::BinaryUploadService::Result result) {
// We add the request here in case we never actually uploaded anything, so it
// wasn't added in OnGetRequestData
safe_browsing::WebUIInfoSingleton::GetInstance()->AddToDeepScanRequests(
request->per_profile_request(), /*access_token*/ "", /*upload_info*/ "",
/*upload_url=*/"", request->content_analysis_request());
safe_browsing::WebUIInfoSingleton::GetInstance()->AddToDeepScanResponses(
/*token=*/"", safe_browsing::BinaryUploadService::ResultToString(result),
enterprise_connectors::ContentAnalysisResponse());
request->FinishRequest(result,
enterprise_connectors::ContentAnalysisResponse());
}
void FilesRequestHandler::UploadFileForDeepScanning(
safe_browsing::BinaryUploadService::Result result,
const base::FilePath& path,
std::unique_ptr<safe_browsing::BinaryUploadService::Request> request) {
safe_browsing::BinaryUploadService* upload_service = GetBinaryUploadService();
if (upload_service)
upload_service->MaybeUploadForDeepScanning(std::move(request));
}
void FilesRequestHandler::FileRequestStartCallback(
size_t index,
const safe_browsing::BinaryUploadService::Request& request) {
start_times_[index] = base::TimeTicks::Now();
}
void FilesRequestHandler::FileRequestCallback(
size_t index,
safe_browsing::BinaryUploadService::Result upload_result,
enterprise_connectors::ContentAnalysisResponse response) {
// Remember to send an ack for this response. It's possible for the response
// to be empty and have no request token. This may happen if Chrome decides
// to allow the file without uploading with the binary upload service. For
// example, zero length files.
if (upload_result == safe_browsing::BinaryUploadService::Result::SUCCESS &&
response.has_request_token()) {
request_tokens_to_ack_final_actions_[response.request_token()] =
GetAckFinalAction(response);
}
DCHECK_EQ(results_.size(), paths_.size());
if (upload_result ==
safe_browsing::BinaryUploadService::Result::TOO_MANY_REQUESTS) {
throttled_ = true;
}
// Find the path in the set of files that are being scanned.
DCHECK_LT(index, paths_.size());
const base::FilePath& path = paths_[index];
const auto start_timestamp = (start_times_[index] != base::TimeTicks::Min())
? start_times_[index]
: upload_start_time_;
const auto& analysis_settings = content_analysis_info_->settings();
RecordDeepScanMetrics(
analysis_settings.cloud_or_local_settings.is_cloud_analysis(),
access_point_, base::TimeTicks::Now() - start_timestamp,
file_info_[index].size, upload_result, response);
RequestHandlerResult request_handler_result =
CalculateRequestHandlerResult(analysis_settings, upload_result, response);
results_[index] = request_handler_result;
++file_result_count_;
bool result_is_warning = request_handler_result.final_result ==
FinalContentAnalysisResult::WARNING;
if (result_is_warning) {
file_warnings_[index] = response;
}
MaybeReportDeepScanningVerdict(
profile_, content_analysis_info_.get(), source_, destination_,
path.AsUTF8Unsafe(), file_info_[index].sha256,
file_info_[index].mime_type, AccessPointToTriggerString(access_point_),
content_transfer_method_,
content_analysis_info_->GetContentAreaAccountEmail(),
file_info_[index].size, content_analysis_info_->referrer_chain(),
upload_result, response,
CalculateEventResult(analysis_settings, request_handler_result.complies,
result_is_warning));
safe_browsing::DecrementCrashKey(
safe_browsing::ScanningCrashKey::PENDING_FILE_UPLOADS);
MaybeCompleteScanRequest();
}
void FilesRequestHandler::MaybeCompleteScanRequest() {
if (file_result_count_ < paths_.size()) {
return;
}
scoped_file_access_.reset();
DCHECK(!callback_.is_null());
std::move(callback_).Run(std::move(results_));
}
void FilesRequestHandler::CreateFileOpeningJob(
std::vector<safe_browsing::FileOpeningJob::FileOpeningTask> tasks,
file_access::ScopedFileAccess file_access) {
scoped_file_access_ =
std::make_unique<file_access::ScopedFileAccess>(std::move(file_access));
file_opening_job_ =
std::make_unique<safe_browsing::FileOpeningJob>(std::move(tasks));
}
} // namespace enterprise_connectors