// 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/file_transfer_analysis_delegate.h"
#include <numeric>
#include <utility>
#include <vector>
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/memory/ptr_util.h"
#include "base/no_destructor.h"
#include "chrome/browser/ash/file_manager/volume_manager.h"
#include "chrome/browser/enterprise/connectors/analysis/files_request_handler.h"
#include "chrome/browser/enterprise/connectors/analysis/source_destination_matcher_ash.h"
#include "chrome/browser/enterprise/connectors/connectors_service.h"
#include "chrome/browser/extensions/api/safe_browsing_private/safe_browsing_private_event_router.h"
#include "chrome/browser/profiles/profile.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 "components/enterprise/connectors/core/analysis_settings.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "storage/browser/file_system/file_system_context.h"
#include "storage/browser/file_system/file_system_operation.h"
#include "storage/browser/file_system/file_system_operation_runner.h"
#include "storage/browser/file_system/file_system_url.h"
#include "storage/browser/file_system/recursive_operation_delegate.h"
using safe_browsing::BinaryUploadService;
namespace {
enterprise_connectors::FileTransferAnalysisDelegate::
FileTransferAnalysisDelegateFactory&
GetFactoryStorage() {
static base::NoDestructor<
enterprise_connectors::FileTransferAnalysisDelegate::
FileTransferAnalysisDelegateFactory>
factory;
return *factory;
}
// GetFileURLsDelegate is used to get the `FileSystemURL`s of all files lying
// within `root`. A vector of these urls is passed to `callback`. If `root` is
// a file, the vector will only contain `root`. If `root` is a directory all
// files lying in that directory or any descended subdirectory are passed to
// `callback`.
class GetFileURLsDelegate final : public storage::RecursiveOperationDelegate {
public:
using FileURLsCallback =
base::OnceCallback<void(std::vector<storage::FileSystemURL>)>;
GetFileURLsDelegate(storage::FileSystemContext* file_system_context,
const storage::FileSystemURL& root,
FileURLsCallback callback)
: RecursiveOperationDelegate(file_system_context),
root_(root),
callback_(std::move(callback)) {}
GetFileURLsDelegate(const GetFileURLsDelegate&) = delete;
GetFileURLsDelegate& operator=(const GetFileURLsDelegate&) = delete;
~GetFileURLsDelegate() override = default;
// RecursiveOperationDelegate:
void Run() override { NOTREACHED_IN_MIGRATION(); }
void RunRecursively() override {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
StartRecursiveOperation(root_,
storage::FileSystemOperation::ERROR_BEHAVIOR_ABORT,
base::BindOnce(&GetFileURLsDelegate::Completed,
weak_ptr_factory_.GetWeakPtr()));
}
void ProcessFile(const storage::FileSystemURL& url,
StatusCallback callback) override {
if (error_url_.is_valid() && error_url_ == url) {
std::move(callback).Run(base::File::FILE_ERROR_FAILED);
return;
}
file_system_context()->operation_runner()->GetMetadata(
url, {storage::FileSystemOperation::GetMetadataField::kIsDirectory},
base::BindOnce(&GetFileURLsDelegate::OnGetMetadata,
weak_ptr_factory_.GetWeakPtr(), url,
std::move(callback)));
}
void ProcessDirectory(const storage::FileSystemURL& url,
StatusCallback callback) override {
std::move(callback).Run(base::File::FILE_OK);
}
void PostProcessDirectory(const storage::FileSystemURL& url,
StatusCallback callback) override {
std::move(callback).Run(base::File::FILE_OK);
}
base::WeakPtr<storage::RecursiveOperationDelegate> AsWeakPtr() override {
return weak_ptr_factory_.GetWeakPtr();
}
private:
void OnGetMetadata(const storage::FileSystemURL& url,
StatusCallback callback,
base::File::Error result,
const base::File::Info& file_info) {
if (result != base::File::FILE_OK) {
std::move(callback).Run(result);
return;
}
if (file_info.is_directory) {
std::move(callback).Run(base::File::FILE_ERROR_NOT_A_FILE);
return;
}
file_urls_.push_back(url);
std::move(callback).Run(base::File::FILE_OK);
}
void Completed(base::File::Error result) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(std::move(callback_), std::move(file_urls_)));
}
storage::FileSystemURL root_;
FileURLsCallback callback_;
storage::FileSystemURL error_url_;
std::vector<storage::FileSystemURL> file_urls_;
base::WeakPtrFactory<GetFileURLsDelegate> weak_ptr_factory_{this};
};
bool IsInSameFileSystem(Profile* profile,
storage::FileSystemURL source_url,
storage::FileSystemURL destination_url) {
// Cheap check: source file system url.
if (!source_url.IsInSameFileSystem(destination_url)) {
return false;
}
// For some URLs FileSystemURL's IsInSameFileSystem function returns false
// positives. Which `volume_manager` is able to properly determine.
file_manager::VolumeManager* const volume_manager =
file_manager::VolumeManager::Get(profile);
base::WeakPtr<file_manager::Volume> source_volume =
volume_manager->FindVolumeFromPath(source_url.path());
base::WeakPtr<file_manager::Volume> destination_volume =
volume_manager->FindVolumeFromPath(destination_url.path());
if (!source_volume || !destination_volume) {
// The source or destination volume don't exist, so we trust the
// FileSystemURL response, i.e., they lie in the same file system.
return true;
}
// If both volumes exist, we check whether their ID is the same.
return source_volume->volume_id() == destination_volume->volume_id();
}
} // namespace
namespace enterprise_connectors {
// static
FileTransferAnalysisDelegate::FileTransferAnalysisResult
FileTransferAnalysisDelegate::FileTransferAnalysisResult::Allowed() {
return FileTransferAnalysisResult(
Verdict::ALLOWED, /*final_result=*/std::nullopt, /*tag=*/std::string());
}
// static
FileTransferAnalysisDelegate::FileTransferAnalysisResult
FileTransferAnalysisDelegate::FileTransferAnalysisResult::Blocked(
FinalContentAnalysisResult final_result,
const std::string& tag) {
return FileTransferAnalysisResult(Verdict::BLOCKED, final_result, tag);
}
// static
FileTransferAnalysisDelegate::FileTransferAnalysisResult
FileTransferAnalysisDelegate::FileTransferAnalysisResult::Unknown() {
return FileTransferAnalysisResult(
Verdict::UNKNOWN, /*final_result=*/std::nullopt, /*tag=*/std::string());
}
const std::string&
FileTransferAnalysisDelegate::FileTransferAnalysisResult::tag() const {
return tag_;
}
const std::optional<FinalContentAnalysisResult>
FileTransferAnalysisDelegate::FileTransferAnalysisResult::final_result() const {
return final_result_;
}
FileTransferAnalysisDelegate::FileTransferAnalysisResult::
FileTransferAnalysisResult(
Verdict verdict,
std::optional<FinalContentAnalysisResult> final_result,
const std::string& tag)
: verdict_(verdict), final_result_(final_result), tag_(tag) {}
FileTransferAnalysisDelegate::FileTransferAnalysisResult::
~FileTransferAnalysisResult() = default;
FileTransferAnalysisDelegate::FileTransferAnalysisResult::
FileTransferAnalysisResult(const FileTransferAnalysisResult& other) =
default;
FileTransferAnalysisDelegate::FileTransferAnalysisResult&
FileTransferAnalysisDelegate::FileTransferAnalysisResult::operator=(
FileTransferAnalysisResult&& other) = default;
bool FileTransferAnalysisDelegate::FileTransferAnalysisResult::IsAllowed()
const {
return verdict_ == Verdict::ALLOWED;
}
bool FileTransferAnalysisDelegate::FileTransferAnalysisResult::IsBlocked()
const {
return verdict_ == Verdict::BLOCKED;
}
bool FileTransferAnalysisDelegate::FileTransferAnalysisResult::IsUnknown()
const {
return verdict_ == Verdict::UNKNOWN;
}
// static
std::unique_ptr<FileTransferAnalysisDelegate>
FileTransferAnalysisDelegate::Create(
safe_browsing::DeepScanAccessPoint access_point,
storage::FileSystemURL source_url,
storage::FileSystemURL destination_url,
Profile* profile,
storage::FileSystemContext* file_system_context,
AnalysisSettings settings) {
if (GetFactoryStorage().is_null()) {
// This code path is always reached outside of tests.
return base::WrapUnique(
new enterprise_connectors::FileTransferAnalysisDelegate(
access_point, source_url, destination_url, profile,
file_system_context, std::move(settings)));
} else {
// Only in tests, GetFactoryStorage() can be set and this code path can be
// reached.
// Pass `idx` in addition to the constructor parameters to make testing
// easier.
return GetFactoryStorage().Run(access_point, source_url, destination_url,
profile, file_system_context,
std::move(settings));
}
}
// static
void FileTransferAnalysisDelegate::SetFactorForTesting(
FileTransferAnalysisDelegateFactory factory) {
GetFactoryStorage() = factory;
}
// static
std::vector<std::optional<AnalysisSettings>>
FileTransferAnalysisDelegate::IsEnabledVec(
Profile* profile,
const std::vector<storage::FileSystemURL>& source_urls,
storage::FileSystemURL destination_url) {
DCHECK(profile);
auto* service =
enterprise_connectors::ConnectorsServiceFactory::GetForBrowserContext(
profile);
// If the corresponding Connector policy isn't set, don't perform scans.
if (!service ||
!service->IsConnectorEnabled(enterprise_connectors::FILE_TRANSFER)) {
// Return an empty vector.
return {};
}
std::vector<std::optional<AnalysisSettings>> settings(source_urls.size());
bool at_least_one_enabled = false;
for (size_t i = 0; i < source_urls.size(); ++i) {
if (IsInSameFileSystem(profile, source_urls[i], destination_url)) {
// Scanning is disabled for transfers on the same file system.
continue;
}
settings[i] = service->GetAnalysisSettings(
source_urls[i], destination_url, enterprise_connectors::FILE_TRANSFER);
at_least_one_enabled |= settings[i].has_value();
}
if (!at_least_one_enabled) {
// Return an empty vector.
return {};
}
return settings;
}
FileTransferAnalysisDelegate::FileTransferAnalysisResult
FileTransferAnalysisDelegate::GetAnalysisResultAfterScan(
storage::FileSystemURL url) {
// Should only be called for blocking scans.
DCHECK_EQ(settings_.block_until_verdict, BlockUntilVerdict::kBlock);
DCHECK_EQ(results_.size(), scanning_urls_.size());
for (size_t i = 0; i < scanning_urls_.size(); ++i) {
if (scanning_urls_[i] == url) {
if (results_[i].complies ||
(warning_is_bypassed_ &&
results_[i].final_result == FinalContentAnalysisResult::WARNING)) {
return FileTransferAnalysisResult::Allowed();
}
return FileTransferAnalysisResult::Blocked(results_[i].final_result,
results_[i].tag);
}
}
return FileTransferAnalysisResult::Unknown();
}
std::vector<storage::FileSystemURL>
FileTransferAnalysisDelegate::GetWarnedFiles() const {
// Should only be called for blocking scans.
DCHECK_EQ(settings_.block_until_verdict, BlockUntilVerdict::kBlock);
DCHECK_EQ(results_.size(), scanning_urls_.size());
std::vector<storage::FileSystemURL> warned_files;
for (size_t i = 0; i < scanning_urls_.size(); ++i) {
if (!results_[i].complies &&
results_[i].final_result == FinalContentAnalysisResult::WARNING) {
warned_files.push_back(scanning_urls_[i]);
}
}
return warned_files;
}
void FileTransferAnalysisDelegate::UploadData(
base::OnceClosure completion_callback) {
callback_ = std::move(completion_callback);
DCHECK(!callback_.is_null());
// This will start aggregating the needed file urls and pass them to
// OnGotFileSourceURLs.
// The usage of the WeakPtr is only safe if `get_file_urls_delegate_` is
// deleted on the IOThread.
content::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&storage::RecursiveOperationDelegate::RunRecursively,
get_file_urls_delegate_->AsWeakPtr()));
}
FileTransferAnalysisDelegate::FileTransferAnalysisDelegate(
safe_browsing::DeepScanAccessPoint access_point,
storage::FileSystemURL source_url,
storage::FileSystemURL destination_url,
Profile* profile,
storage::FileSystemContext* file_system_context,
AnalysisSettings settings)
: settings_{std::move(settings)},
profile_{profile},
access_point_{access_point},
source_url_(std::move(source_url)),
destination_url_{std::move(destination_url)} {
DCHECK(profile);
// For blocking scans, scanning is performed before the copy/move and
// thus scanning should be performed on the source.
// For non-blocking report-only scans, scanning is performed after the
// copy/move and thus scanning should be performed on the destination.
auto scanning_url = settings_.block_until_verdict == BlockUntilVerdict::kBlock
? source_url_
: destination_url_;
get_file_urls_delegate_ = std::make_unique<GetFileURLsDelegate>(
file_system_context, scanning_url,
base::BindOnce(&FileTransferAnalysisDelegate::OnGotFileURLs,
weak_ptr_factory_.GetWeakPtr()));
}
void FileTransferAnalysisDelegate::BypassWarnings(
std::optional<std::u16string> user_justification) {
if (!warned_file_indices_.empty()) {
request_handler_->ReportWarningBypass(user_justification);
warning_is_bypassed_ = true;
}
}
void FileTransferAnalysisDelegate::Cancel(bool warning) {
// TODO(crbug.com/1340313)
}
std::optional<std::u16string> FileTransferAnalysisDelegate::GetCustomMessage(
const std::string& tag) const {
auto it = settings_.tags.find(tag);
if (it == settings_.tags.end()) {
return std::nullopt;
}
const std::u16string& message = it->second.custom_message.message;
if (message.empty()) {
return std::nullopt;
}
return message;
}
std::optional<GURL> FileTransferAnalysisDelegate::GetCustomLearnMoreUrl(
const std::string& tag) const {
auto it = settings_.tags.find(tag);
if (it == settings_.tags.end()) {
return std::nullopt;
}
const GURL& learn_more_url = it->second.custom_message.learn_more_url;
if (!learn_more_url.is_valid()) {
return std::nullopt;
}
return learn_more_url;
}
bool FileTransferAnalysisDelegate::BypassRequiresJustification(
const std::string& tag) const {
auto it = settings_.tags.find(tag);
if (it == settings_.tags.end()) {
return false;
}
return it->second.requires_justification;
}
FilesRequestHandler*
FileTransferAnalysisDelegate::GetFilesRequestHandlerForTesting() {
return request_handler_.get();
}
void FileTransferAnalysisDelegate::OnGotFileURLs(
std::vector<storage::FileSystemURL> scanning_urls) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
scanning_urls_ = std::move(scanning_urls);
if (scanning_urls_.empty()) {
ContentAnalysisCompleted(std::vector<RequestHandlerResult>());
return;
}
std::vector<base::FilePath> paths;
for (const storage::FileSystemURL& url : scanning_urls_) {
paths.push_back(url.path());
}
request_handler_ = FilesRequestHandler::Create(
safe_browsing::BinaryUploadService::GetForProfile(profile_, settings_),
profile_, settings_, GURL{},
SourceDestinationMatcherAsh::GetVolumeDescriptionFromPath(
profile_, source_url_.path()),
SourceDestinationMatcherAsh::GetVolumeDescriptionFromPath(
profile_, destination_url_.path()),
// User action id and tab title are only needed for local content
// analysis, leave them empty here.
/*user_action_id=*/std::string(), /*tab_title=*/std::string(),
/*content_transfer_method=*/std::string(), access_point_,
ContentAnalysisRequest::UNKNOWN, std::move(paths),
base::BindOnce(&FileTransferAnalysisDelegate::ContentAnalysisCompleted,
weak_ptr_factory_.GetWeakPtr()));
request_handler_->UploadData();
}
FileTransferAnalysisDelegate::~FileTransferAnalysisDelegate() {
if (get_file_urls_delegate_) {
// To ensure that there are no race conditions, we post the deletion of
// `get_file_urls_delegate_` to the IO thread.
content::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
[](std::unique_ptr<storage::RecursiveOperationDelegate> delegate) {
// Do nothing.
// At the end of this task `get_file_urls_delegate_`
// will be deleted.
},
std::move(get_file_urls_delegate_)));
}
}
void FileTransferAnalysisDelegate::ContentAnalysisCompleted(
std::vector<RequestHandlerResult> results) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
results_ = std::move(results);
// Don't show warning here, as we use multiple FileTransferAnalysisDelegate's
// and only want to show one warning.
for (size_t index = 0; index < results_.size(); ++index) {
FinalContentAnalysisResult result = results_[index].final_result;
if (result == FinalContentAnalysisResult::WARNING) {
warned_file_indices_.push_back(index);
}
}
DCHECK(!callback_.is_null());
std::move(callback_).Run();
}
} // namespace enterprise_connectors