blob: ba0e3135e039f2df4ed0fa7b1a0b7a68624b569b [file] [log] [blame]
// 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/enterprise/data_controls/reporting_service.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/profiles/profile.h"
#include "components/enterprise/data_controls/core/browser/prefs.h"
#include "components/enterprise/data_controls/core/browser/verdict.h"
#include "components/policy/core/common/policy_types.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/clipboard_types.h"
#include "ui/base/clipboard/clipboard_constants.h"
#include "ui/base/clipboard/clipboard_format_type.h"
namespace data_controls {
namespace {
// TODO(b/311679168): Move this to share logic with ContentAnalysisDelegate.
std::string GetMimeType(const ui::ClipboardFormatType& clipboard_format) {
if (clipboard_format == ui::ClipboardFormatType::PlainTextType()) {
return ui::kMimeTypePlainText;
} else if (clipboard_format == ui::ClipboardFormatType::HtmlType()) {
return ui::kMimeTypeHtml;
} else if (clipboard_format == ui::ClipboardFormatType::SvgType()) {
return ui::kMimeTypeSvg;
} else if (clipboard_format == ui::ClipboardFormatType::RtfType()) {
return ui::kMimeTypeRtf;
} else if (clipboard_format == ui::ClipboardFormatType::PngType()) {
return ui::kMimeTypePng;
} else if (clipboard_format == ui::ClipboardFormatType::FilenamesType()) {
return ui::kMimeTypeUriList;
}
return "";
}
GURL GetURL(const content::ClipboardEndpoint& endpoint) {
if (!endpoint.data_transfer_endpoint() ||
!endpoint.data_transfer_endpoint()->IsUrlType() ||
!endpoint.data_transfer_endpoint()->GetURL()) {
return GURL();
}
return *endpoint.data_transfer_endpoint()->GetURL();
}
enterprise_connectors::EventResult GetEventResult(Rule::Level level) {
switch (level) {
case Rule::Level::kNotSet:
case Rule::Level::kAllow:
case Rule::Level::kReport:
return enterprise_connectors::EventResult::ALLOWED;
case Rule::Level::kBlock:
return enterprise_connectors::EventResult::BLOCKED;
case Rule::Level::kWarn:
return enterprise_connectors::EventResult::WARNED;
}
}
bool PolicyAppliedAtUserScope(content::BrowserContext* browser_context,
const char* scope_pref) {
CHECK(browser_context);
CHECK(scope_pref);
return Profile::FromBrowserContext(browser_context)
->GetPrefs()
->GetInteger(scope_pref) == policy::POLICY_SCOPE_USER;
}
} // namespace
// -------------------------------
// ReportingService implementation
// -------------------------------
// static
enterprise_connectors::ContentMetaData::CopiedTextSource
ReportingService::GetClipboardSource(
const content::ClipboardEndpoint& source,
const content::ClipboardEndpoint& destination,
const char* scope_pref) {
CHECK(destination.browser_context());
using SourceType = enterprise_connectors::ContentMetaData::CopiedTextSource;
enterprise_connectors::ContentMetaData::CopiedTextSource copied_text_source;
if (!source.browser_context()) {
copied_text_source.set_context(SourceType::CLIPBOARD);
} else if (Profile::FromBrowserContext(source.browser_context())
->IsIncognitoProfile()) {
copied_text_source.set_context(SourceType::INCOGNITO);
} else if (source.browser_context() == destination.browser_context()) {
copied_text_source.set_context(SourceType::SAME_PROFILE);
} else {
copied_text_source.set_context(SourceType::OTHER_PROFILE);
}
switch (copied_text_source.context()) {
case SourceType::UNSPECIFIED:
case SourceType::INCOGNITO:
case SourceType::CLIPBOARD:
break;
case SourceType::OTHER_PROFILE:
// Only add a source URL if the other profile is getting the policy
// applied at the machine scope, not the user scope.
if (PolicyAppliedAtUserScope(destination.browser_context(), scope_pref)) {
break;
}
[[fallthrough]];
case SourceType::SAME_PROFILE:
if (source.data_transfer_endpoint() &&
source.data_transfer_endpoint()->IsUrlType() &&
source.data_transfer_endpoint()->GetURL()) {
copied_text_source.set_url(
source.data_transfer_endpoint()->GetURL()->spec());
}
break;
}
return copied_text_source;
}
// static
std::string ReportingService::GetClipboardSourceString(
const content::ClipboardEndpoint& source,
const content::ClipboardEndpoint& destination,
const char* scope_pref) {
return GetClipboardSourceString(
GetClipboardSource(source, destination, scope_pref));
}
// static
std::string ReportingService::GetClipboardSourceString(
const enterprise_connectors::ContentMetaData::CopiedTextSource& source) {
if (!source.url().empty()) {
return source.url();
}
switch (source.context()) {
case enterprise_connectors::ContentMetaData::CopiedTextSource::UNSPECIFIED:
case enterprise_connectors::ContentMetaData::CopiedTextSource::SAME_PROFILE:
return "";
case enterprise_connectors::ContentMetaData::CopiedTextSource::INCOGNITO:
return "INCOGNITO";
case enterprise_connectors::ContentMetaData::CopiedTextSource::CLIPBOARD:
return "CLIPBOARD";
case enterprise_connectors::ContentMetaData::CopiedTextSource::
OTHER_PROFILE:
return "OTHER_PROFILE";
}
}
ReportingService::ReportingService(content::BrowserContext& browser_context)
: profile_(*Profile::FromBrowserContext(&browser_context)) {}
ReportingService::~ReportingService() = default;
void ReportingService::ReportPaste(
const content::ClipboardEndpoint& source,
const content::ClipboardEndpoint& destination,
const content::ClipboardMetadata& metadata,
const Verdict& verdict) {
ReportCopyOrPaste(
source, destination, metadata, verdict,
extensions::SafeBrowsingPrivateEventRouter::kTriggerWebContentUpload,
GetEventResult(verdict.level()));
}
void ReportingService::ReportPasteWarningBypassed(
const content::ClipboardEndpoint& source,
const content::ClipboardEndpoint& destination,
const content::ClipboardMetadata& metadata,
const Verdict& verdict) {
ReportCopyOrPaste(
source, destination, metadata, verdict,
extensions::SafeBrowsingPrivateEventRouter::kTriggerWebContentUpload,
enterprise_connectors::EventResult::BYPASSED);
}
void ReportingService::ReportCopy(const content::ClipboardEndpoint& source,
const content::ClipboardMetadata& metadata,
const Verdict& verdict) {
ReportCopyOrPaste(
source, /*destination=*/std::nullopt, metadata, verdict,
extensions::SafeBrowsingPrivateEventRouter::kTriggerClipboardCopy,
GetEventResult(verdict.level()));
}
void ReportingService::ReportCopyWarningBypassed(
const content::ClipboardEndpoint& source,
const content::ClipboardMetadata& metadata,
const Verdict& verdict) {
ReportCopyOrPaste(
source, /*destination=*/std::nullopt, metadata, verdict,
extensions::SafeBrowsingPrivateEventRouter::kTriggerClipboardCopy,
enterprise_connectors::EventResult::BYPASSED);
}
void ReportingService::ReportCopyOrPaste(
const content::ClipboardEndpoint& source,
const std::optional<content::ClipboardEndpoint>& destination,
const content::ClipboardMetadata& metadata,
const Verdict& verdict,
const std::string& trigger,
enterprise_connectors::EventResult event_result) {
auto* router =
extensions::SafeBrowsingPrivateEventRouterFactory::GetForProfile(
&profile_.get());
if (!router || verdict.triggered_rules().empty()) {
return;
}
GURL url;
std::string destination_string;
std::string source_string;
if (trigger ==
extensions::SafeBrowsingPrivateEventRouter::kTriggerWebContentUpload) {
DCHECK(destination.has_value());
url = GetURL(*destination);
destination_string = url.spec();
source_string = GetClipboardSourceString(source, *destination,
kDataControlsRulesScopePref);
} else {
DCHECK_EQ(
trigger,
extensions::SafeBrowsingPrivateEventRouter::kTriggerClipboardCopy);
DCHECK(!destination.has_value());
url = GetURL(source);
source_string = GetURL(source).spec();
}
router->OnDataControlsSensitiveDataEvent(
/*url=*/url,
/*tab_url=*/url,
/*source=*/source_string,
/*destination=*/destination_string,
/*mime_type=*/GetMimeType(metadata.format_type),
/*trigger=*/trigger,
/*triggered_rules=*/verdict.triggered_rules(),
/*event_result=*/event_result,
/*content_size=*/metadata.size.value_or(-1));
}
// --------------------------------------
// ReportingServiceFactory implementation
// --------------------------------------
ReportingServiceBase* ReportingServiceFactory::GetForBrowserContext(
content::BrowserContext* context) {
return static_cast<ReportingServiceBase*>(
GetInstance()->GetServiceForBrowserContext(context, /*create=*/true));
}
// static
ReportingServiceFactory* ReportingServiceFactory::GetInstance() {
static base::NoDestructor<ReportingServiceFactory> instance;
return instance.get();
}
ReportingServiceFactory::ReportingServiceFactory()
: ProfileKeyedServiceFactory(
"DataControlsReportingService",
// `kOriginalOnly` is used since there is no reporting done for
// incognito profiles.
ProfileSelections::Builder()
.WithRegular(ProfileSelection::kOriginalOnly)
.WithGuest(ProfileSelection::kNone)
.WithSystem(ProfileSelection::kNone)
.WithAshInternals(ProfileSelection::kNone)
.Build()) {
DependsOn(extensions::SafeBrowsingPrivateEventRouterFactory::GetInstance());
}
ReportingServiceFactory::~ReportingServiceFactory() = default;
std::unique_ptr<KeyedService>
ReportingServiceFactory::BuildServiceInstanceForBrowserContext(
content::BrowserContext* context) const {
return base::WrapUnique(new ReportingService(*context));
}
} // namespace data_controls