| // 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/common.h" |
| |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/to_string.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "chrome/browser/enterprise/connectors/connectors_service.h" |
| #include "chrome/browser/enterprise/connectors/test/deep_scanning_test_utils.h" |
| #include "chrome/browser/policy/dm_token_utils.h" |
| #include "chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_utils.h" |
| #include "chrome/test/base/testing_browser_process.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "chrome/test/base/testing_profile_manager.h" |
| #include "components/download/public/common/download_danger_type.h" |
| #include "components/download/public/common/mock_download_item.h" |
| #include "components/enterprise/common/proto/connectors.pb.h" |
| #include "components/enterprise/connectors/core/features.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "content/public/test/fake_download_item.h" |
| #include "content/public/test/navigation_simulator.h" |
| #include "content/public/test/test_renderer_host.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/public/test/web_contents_tester.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace enterprise_connectors { |
| |
| namespace { |
| |
| #if BUILDFLAG(ENTERPRISE_CONTENT_ANALYSIS) |
| struct CustomMessageTestCase { |
| TriggeredRule::Action action; |
| std::string message; |
| }; |
| |
| constexpr char kDmToken[] = "dm_token"; |
| constexpr char kTestUrl[] = "http://example.com/"; |
| constexpr char kTestInvalidUrl[] = "example.com"; |
| constexpr char kTestMessage[] = "test"; |
| constexpr char16_t kU16TestMessage[] = u"test"; |
| constexpr char kTestMessage2[] = "test2"; |
| constexpr char kGoogleServiceProvider[] = R"( |
| { |
| "service_provider": "google", |
| "enable": [ |
| { |
| "url_list": ["*"], |
| "tags": ["dlp"] |
| } |
| ], |
| "block_large_files": 1 |
| })"; |
| constexpr char kTestEscapedHtmlMessage[] = "<>&"'"; |
| constexpr char16_t kTestUnescapedHtmlMessage[] = u"<>&\"'"; |
| // Offset to first placeholder index for |
| // IDS_DEEP_SCANNING_DIALOG_CUSTOM_MESSAGE. |
| constexpr size_t kRuleMessageOffset = 26; |
| constexpr char kTestLinkedMessage[] = "Learn More"; |
| constexpr char16_t kU16TestLinkedMessage[] = u"Learn More"; |
| |
| ContentAnalysisResponse CreateContentAnalysisResponse( |
| const std::vector<CustomMessageTestCase>& triggered_rules, |
| const std::string& url) { |
| ContentAnalysisResponse response; |
| auto* result = response.add_results(); |
| result->set_tag("dlp"); |
| result->set_status( |
| enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS); |
| |
| for (const auto& triggered_rule : triggered_rules) { |
| auto* rule = result->add_triggered_rules(); |
| rule->set_action(triggered_rule.action); |
| if (!triggered_rule.message.empty()) { |
| ContentAnalysisResponse::Result::TriggeredRule::CustomRuleMessage |
| custom_message; |
| auto* custom_segment = custom_message.add_message_segments(); |
| custom_segment->set_text(triggered_rule.message); |
| auto* custom_linked_segment = custom_message.add_message_segments(); |
| custom_linked_segment->set_text(kTestLinkedMessage); |
| custom_linked_segment->set_link(url); |
| *rule->mutable_custom_rule_message() = custom_message; |
| } |
| } |
| return response; |
| } |
| |
| class BaseTest : public testing::Test { |
| public: |
| BaseTest() : profile_manager_(TestingBrowserProcess::GetGlobal()) { |
| EXPECT_TRUE(profile_manager_.SetUp()); |
| profile_ = profile_manager_.CreateTestingProfile("test-user"); |
| } |
| |
| void EnableFeatures() { scoped_feature_list_.Reset(); } |
| |
| Profile* profile() { return profile_; } |
| |
| protected: |
| content::BrowserTaskEnvironment task_environment_; |
| base::test::ScopedFeatureList scoped_feature_list_; |
| TestingProfileManager profile_manager_; |
| raw_ptr<TestingProfile> profile_; |
| }; |
| #endif // BUILDFLAG(ENTERPRISE_CONTENT_ANALYSIS) |
| |
| } // namespace |
| |
| #if BUILDFLAG(ENTERPRISE_CONTENT_ANALYSIS) |
| class EnterpriseConnectorsResultShouldAllowDataUseTest |
| : public BaseTest, |
| public testing::WithParamInterface<bool> { |
| public: |
| EnterpriseConnectorsResultShouldAllowDataUseTest() = default; |
| |
| void SetUp() override { |
| BaseTest::SetUp(); |
| EnableFeatures(); |
| |
| // Settings can't be returned if no DM token exists. |
| SetDMTokenForTesting(policy::DMToken::CreateValidToken(kDmToken)); |
| } |
| |
| bool allowed() const { return !GetParam(); } |
| std::string bool_setting() const { return base::ToString(GetParam()); } |
| const char* default_action_setting() const { |
| return GetParam() ? "block" : "allow"; |
| } |
| |
| AnalysisSettings settings() { |
| std::optional<AnalysisSettings> settings = |
| ConnectorsServiceFactory::GetForBrowserContext(profile()) |
| ->GetAnalysisSettings(GURL(kTestUrl), FILE_ATTACHED); |
| EXPECT_TRUE(settings.has_value()); |
| return std::move(settings.value()); |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(, |
| EnterpriseConnectorsResultShouldAllowDataUseTest, |
| testing::Bool()); |
| |
| TEST_P(EnterpriseConnectorsResultShouldAllowDataUseTest, BlockLargeFile) { |
| auto pref = base::StringPrintf(R"( |
| { |
| "service_provider": "google", |
| "enable": [{"url_list": ["*"], "tags": ["dlp"]}], |
| "block_large_files": %s |
| })", |
| bool_setting()); |
| test::SetAnalysisConnector(profile()->GetPrefs(), FILE_ATTACHED, pref); |
| EXPECT_EQ(allowed(), |
| ResultShouldAllowDataUse( |
| settings(), |
| safe_browsing::BinaryUploadService::Result::FILE_TOO_LARGE)); |
| } |
| |
| TEST_P(EnterpriseConnectorsResultShouldAllowDataUseTest, |
| BlockPasswordProtected) { |
| auto pref = base::StringPrintf(R"( |
| { |
| "service_provider": "google", |
| "enable": [{"url_list": ["*"], "tags": ["dlp"]}], |
| "block_password_protected": %s |
| })", |
| bool_setting()); |
| test::SetAnalysisConnector(profile()->GetPrefs(), FILE_ATTACHED, pref); |
| EXPECT_EQ(allowed(), |
| ResultShouldAllowDataUse( |
| settings(), |
| safe_browsing::BinaryUploadService::Result::FILE_ENCRYPTED)); |
| } |
| |
| TEST_P(EnterpriseConnectorsResultShouldAllowDataUseTest, BlockUploadFailure) { |
| auto pref = base::StringPrintf(R"( |
| { |
| "service_provider": "google", |
| "enable": [{"url_list": ["*"], "tags": ["dlp"]}], |
| "default_action": "%s" |
| })", |
| default_action_setting()); |
| |
| test::SetAnalysisConnector(profile()->GetPrefs(), FILE_ATTACHED, pref); |
| EXPECT_EQ(allowed(), |
| ResultShouldAllowDataUse( |
| settings(), |
| safe_browsing::BinaryUploadService::Result::UPLOAD_FAILURE)); |
| } |
| |
| class ContentAnalysisResponseCustomMessageTest |
| : public BaseTest, |
| public testing::WithParamInterface< |
| std::tuple<std::vector<CustomMessageTestCase>, std::u16string>> { |
| public: |
| ContentAnalysisResponseCustomMessageTest() = default; |
| |
| void SetUp() override { |
| BaseTest::SetUp(); |
| EnableFeatures(); |
| |
| // Settings can't be returned if no DM token exists. |
| SetDMTokenForTesting(policy::DMToken::CreateValidToken(kDmToken)); |
| } |
| |
| std::vector<CustomMessageTestCase> triggered_rules() const { |
| return std::get<0>(GetParam()); |
| } |
| std::u16string expected_message() const { return std::get<1>(GetParam()); } |
| |
| AnalysisSettings settings() { |
| std::optional<AnalysisSettings> settings = |
| ConnectorsServiceFactory::GetForBrowserContext(profile()) |
| ->GetAnalysisSettings(GURL(kTestUrl), FILE_ATTACHED); |
| EXPECT_TRUE(settings.has_value()); |
| return std::move(settings.value()); |
| } |
| }; |
| |
| TEST_P(ContentAnalysisResponseCustomMessageTest, ValidUrlCustomMessage) { |
| test::SetAnalysisConnector(profile()->GetPrefs(), FILE_ATTACHED, |
| kGoogleServiceProvider); |
| ContentAnalysisResponse response = |
| CreateContentAnalysisResponse(triggered_rules(), kTestUrl); |
| RequestHandlerResult result = CalculateRequestHandlerResult( |
| settings(), safe_browsing::BinaryUploadService::Result::SUCCESS, |
| response); |
| std::u16string custom_message = |
| GetCustomRuleString(result.custom_rule_message); |
| std::vector<std::pair<gfx::Range, GURL>> custom_ranges = |
| GetCustomRuleStyles(result.custom_rule_message, kRuleMessageOffset); |
| |
| EXPECT_EQ(custom_message, |
| custom_message.empty() |
| ? std::u16string{} |
| : base::StrCat({expected_message(), kU16TestLinkedMessage})); |
| |
| if (custom_message.empty()) { |
| EXPECT_TRUE(custom_ranges.empty()); |
| } else { |
| EXPECT_EQ(1u, custom_ranges.size()); |
| EXPECT_EQ(strlen(kTestLinkedMessage), |
| custom_ranges.begin()->first.length()); |
| EXPECT_EQ(custom_ranges.begin()->first.start(), |
| expected_message().length() + kRuleMessageOffset); |
| } |
| } |
| |
| TEST_P(ContentAnalysisResponseCustomMessageTest, InvalidUrlCustomMessage) { |
| test::SetAnalysisConnector(profile()->GetPrefs(), FILE_ATTACHED, |
| kGoogleServiceProvider); |
| ContentAnalysisResponse response = |
| CreateContentAnalysisResponse(triggered_rules(), kTestInvalidUrl); |
| RequestHandlerResult result = CalculateRequestHandlerResult( |
| settings(), safe_browsing::BinaryUploadService::Result::SUCCESS, |
| response); |
| std::u16string custom_message = |
| GetCustomRuleString(result.custom_rule_message); |
| std::vector<std::pair<gfx::Range, GURL>> custom_ranges = |
| GetCustomRuleStyles(result.custom_rule_message, kRuleMessageOffset); |
| |
| EXPECT_EQ(custom_message, |
| custom_message.empty() |
| ? std::u16string{} |
| : base::StrCat({expected_message(), kU16TestLinkedMessage})); |
| EXPECT_TRUE(custom_ranges.empty()); |
| } |
| |
| TEST_P(ContentAnalysisResponseCustomMessageTest, DownloadsItemCustomMessage) { |
| ContentAnalysisResponse response = |
| CreateContentAnalysisResponse(triggered_rules(), kTestUrl); |
| download::DownloadDangerType danger_type; |
| TriggeredRule::Action action = GetHighestPrecedenceAction(response, nullptr); |
| if (action == TriggeredRule::WARN) { |
| danger_type = download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_WARNING; |
| } else { |
| danger_type = download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_BLOCK; |
| } |
| |
| // Create download item |
| testing::NiceMock<download::MockDownloadItem> item; |
| enterprise_connectors::FileMetadata file_metadata( |
| "examplename", "12345678", "fake/mimetype", 1234, response); |
| auto scan_result = std::make_unique<enterprise_connectors::ScanResult>( |
| std::move(file_metadata)); |
| item.SetUserData(enterprise_connectors::ScanResult::kKey, |
| std::move(scan_result)); |
| |
| auto custom_rule_message = GetDownloadsCustomRuleMessage(&item, danger_type); |
| if (custom_rule_message.has_value()) { |
| EXPECT_EQ(GetCustomRuleString(custom_rule_message.value()), |
| base::StrCat({expected_message(), kU16TestLinkedMessage})); |
| } else { |
| EXPECT_EQ(std::u16string{}, expected_message()); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| , |
| ContentAnalysisResponseCustomMessageTest, |
| testing::Values( |
| std::make_tuple(std::vector<CustomMessageTestCase>(), |
| /*expected_message=*/std::u16string{}), |
| std::make_tuple( |
| std::vector<CustomMessageTestCase>{ |
| {.action = TriggeredRule::WARN, .message = ""}}, |
| /*expected_message=*/std::u16string{}), |
| std::make_tuple( |
| std::vector<CustomMessageTestCase>{ |
| {.action = TriggeredRule::WARN, .message = kTestMessage}}, |
| /*expected_message=*/kU16TestMessage), |
| std::make_tuple( |
| std::vector<CustomMessageTestCase>{ |
| {.action = TriggeredRule::BLOCK, .message = ""}, |
| {.action = TriggeredRule::WARN, .message = kTestMessage}}, |
| /*expected_message=*/std::u16string{}), |
| std::make_tuple( |
| std::vector<CustomMessageTestCase>{ |
| {.action = TriggeredRule::BLOCK, .message = ""}, |
| {.action = TriggeredRule::BLOCK, .message = kTestMessage}}, |
| /*expected_message=*/kU16TestMessage), |
| std::make_tuple( |
| std::vector<CustomMessageTestCase>{ |
| {.action = TriggeredRule::BLOCK, .message = kTestMessage}, |
| {.action = TriggeredRule::WARN, .message = kTestMessage2}}, |
| /*expected_message=*/kU16TestMessage), |
| std::make_tuple( |
| std::vector<CustomMessageTestCase>{ |
| {.action = TriggeredRule::BLOCK, |
| .message = kTestEscapedHtmlMessage}}, |
| /*expected_message=*/kTestUnescapedHtmlMessage))); |
| |
| class CollectFrameUrlsTest : public BaseTest { |
| public: |
| CollectFrameUrlsTest() = default; |
| |
| protected: |
| void SetUp() override { |
| scoped_feature_list_.InitAndEnableFeature(kEnterpriseIframeDlpRulesSupport); |
| BaseTest::SetUp(); |
| |
| // Create test web contents as we need to navigate to a URL to get a valid |
| // frame chain. |
| web_contents_ = content::WebContentsTester::CreateTestWebContents( |
| profile(), content::SiteInstance::Create(profile())); |
| content::WebContentsTester::For(web_contents_.get()) |
| ->NavigateAndCommit(GURL(kTestUrl)); |
| } |
| |
| void TearDown() override { |
| // `WebContentsTester` needs to be destroyed before the |
| // `RenderViewHostTestEnabler`. |
| if (web_contents_) { |
| web_contents_.reset(); |
| } |
| BaseTest::TearDown(); |
| } |
| |
| content::WebContents* web_contents() { return web_contents_.get(); } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| std::unique_ptr<content::WebContents> web_contents_; |
| // Needed for frame tree and navigation operations. |
| content::RenderViewHostTestEnabler rvh_test_enabler_; |
| }; |
| |
| TEST_F(CollectFrameUrlsTest, NestedFramesWithUninterestingUrl) { |
| base::HistogramTester histogram_tester; |
| |
| // Create test URLs. |
| const GURL child_frame_url1("http://child1.example.com/"); |
| const GURL child_frame_url2("http://child2.example.com/"); |
| const GURL uninteresting_url("about:blank"); |
| |
| // Create main frame. |
| content::RenderFrameHost* main_frame = web_contents()->GetPrimaryMainFrame(); |
| content::RenderFrameHostTester* main_frame_tester = |
| content::RenderFrameHostTester::For(main_frame); |
| |
| // Create and navigate the first child frame. |
| content::RenderFrameHost* child_frame1 = |
| main_frame_tester->AppendChild("child1"); |
| content::NavigationSimulator::NavigateAndCommitFromDocument(child_frame_url1, |
| child_frame1); |
| |
| // Create and navigate the second (nested) child frame. |
| content::RenderFrameHostTester* child_frame1_tester = |
| content::RenderFrameHostTester::For(child_frame1); |
| content::RenderFrameHost* child_frame2 = |
| child_frame1_tester->AppendChild("child2"); |
| content::NavigationSimulator::NavigateAndCommitFromDocument(child_frame_url2, |
| child_frame2); |
| |
| // Create and navigate the third (nested) child frame with uninteresting URL. |
| content::RenderFrameHostTester* child_frame2_tester = |
| content::RenderFrameHostTester::For(child_frame2); |
| content::RenderFrameHost* child_frame3 = |
| child_frame2_tester->AppendChild("child3"); |
| content::NavigationSimulator::NavigateAndCommitFromDocument(uninteresting_url, |
| child_frame3); |
| |
| // Set focus on the innermost frame. |
| content::FocusWebContentsOnFrame(web_contents(), child_frame3); |
| |
| google::protobuf::RepeatedPtrField<std::string> frame_urls = |
| CollectFrameUrls(web_contents(), DeepScanAccessPoint::DOWNLOAD); |
| |
| // Verify that the URL chain is listed in the right order and chain size |
| // histogram is recorded properly. The uninteresting URL and the tab URL |
| // should not be included in the chain. |
| ASSERT_EQ(2, frame_urls.size()); |
| EXPECT_EQ(child_frame_url2.spec(), frame_urls[0]); |
| EXPECT_EQ(child_frame_url1.spec(), frame_urls[1]); |
| |
| // We still include the tab URL in the histogram chain size. |
| histogram_tester.ExpectTotalCount( |
| "Enterprise.IframeDlpRulesSupport.Download.UrlChainSize", 1); |
| histogram_tester.ExpectBucketCount( |
| "Enterprise.IframeDlpRulesSupport.Download.UrlChainSize", 3, 1); |
| } |
| |
| TEST_F(CollectFrameUrlsTest, TabUrlOnly) { |
| base::HistogramTester histogram_tester; |
| |
| google::protobuf::RepeatedPtrField<std::string> frame_urls = |
| CollectFrameUrls(web_contents(), DeepScanAccessPoint::DOWNLOAD); |
| |
| // The URL chain should be empty since there are no iframes. |
| EXPECT_EQ(0, frame_urls.size()); |
| |
| // The histogram should have a value of 1 to account for the tab's URL. |
| histogram_tester.ExpectTotalCount( |
| "Enterprise.IframeDlpRulesSupport.Download.UrlChainSize", 1); |
| histogram_tester.ExpectBucketCount( |
| "Enterprise.IframeDlpRulesSupport.Download.UrlChainSize", 1, 1); |
| } |
| |
| TEST_F(CollectFrameUrlsTest, NoWebContents) { |
| base::HistogramTester histogram_tester; |
| |
| google::protobuf::RepeatedPtrField<std::string> frame_urls = |
| CollectFrameUrls(nullptr, DeepScanAccessPoint::DOWNLOAD); |
| |
| // Since there are no tabs, there should not be any URLs recorded. |
| EXPECT_EQ(0, frame_urls.size()); |
| |
| histogram_tester.ExpectTotalCount( |
| "Enterprise.IframeDlpRulesSupport.Download.UrlChainSize", 1); |
| histogram_tester.ExpectBucketCount( |
| "Enterprise.IframeDlpRulesSupport.Download.UrlChainSize", 0, 1); |
| } |
| #endif // BUILDFLAG(ENTERPRISE_CONTENT_ANALYSIS) |
| |
| } // namespace enterprise_connectors |