diff --git a/toolkit/components/contentanalysis/ContentAnalysis.cpp b/toolkit/components/contentanalysis/ContentAnalysis.cpp index 4f7a21812c8a..4b2a0a0fd382 100644 --- a/toolkit/components/contentanalysis/ContentAnalysis.cpp +++ b/toolkit/components/contentanalysis/ContentAnalysis.cpp @@ -23,6 +23,7 @@ #include "mozilla/dom/Promise.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/glean/ContentanalysisMetrics.h" #include "mozilla/Logging.h" #include "mozilla/ScopeExit.h" #include "mozilla/Services.h" @@ -77,6 +78,14 @@ const char* kPipePathNamePref = "browser.contentanalysis.pipe_path_name"; const char* kClientSignature = "browser.contentanalysis.client_signature"; const char* kAllowUrlPref = "browser.contentanalysis.allow_url_regex_list"; const char* kDenyUrlPref = "browser.contentanalysis.deny_url_regex_list"; +const char* kAgentNamePref = "browser.contentanalysis.agent_name"; +const char* kInterceptionPointPrefNames[] = { + "browser.contentanalysis.interception_point.clipboard.enabled", + "browser.contentanalysis.interception_point.download.enabled", + "browser.contentanalysis.interception_point.drag_and_drop.enabled", + "browser.contentanalysis.interception_point.file_upload.enabled", + "browser.contentanalysis.interception_point.print.enabled", +}; // Allow up to this many threads to be concurrently engaged in synchronous // communcations with the agent. That limit is set by @@ -1445,6 +1454,10 @@ bool ContentAnalysis::IsShutDown() { return *lock; } +NS_IMETHODIMP ContentAnalysis::ForceRecreateClientForTest() { + return CreateClientIfNecessary(/* aForceCreate */ true); +} + nsresult ContentAnalysis::CreateClientIfNecessary( bool aForceCreate /* = false */) { AssertIsOnMainThread(); @@ -1479,6 +1492,7 @@ nsresult ContentAnalysis::CreateClientIfNecessary( nsString clientSignature; // It's OK if this fails, we will default to the empty string Preferences::GetString(kClientSignature, clientSignature); + RecordConnectionSettingsTelemetry(clientSignature); LOGD("Dispatching background task to create Content Analysis client"); rv = NS_DispatchBackgroundTask(NS_NewCancelableRunnableFunction( "ContentAnalysis::CreateContentAnalysisClient", @@ -1494,6 +1508,59 @@ nsresult ContentAnalysis::CreateClientIfNecessary( return NS_OK; } +void ContentAnalysis::RecordConnectionSettingsTelemetry( + const nsString& clientSignature) { + AssertIsOnMainThread(); + { + nsCString agentName; + Preferences::GetCString(kAgentNamePref, agentName); + glean::content_analysis::agent_name.Set(agentName); + } + AutoTArray interceptionPointsOff; + for (const char* interceptionPointPrefName : kInterceptionPointPrefNames) { + bool interceptionPointPrefValue; + Preferences::GetBool(interceptionPointPrefName, + &interceptionPointPrefValue); + if (!interceptionPointPrefValue) { + interceptionPointsOff.AppendElement(interceptionPointPrefName); + } + } + if (!interceptionPointsOff.IsEmpty()) { + glean::content_analysis::interception_points_turned_off.Set( + interceptionPointsOff); + } + glean::content_analysis::show_blocked_result.Set( + StaticPrefs::browser_contentanalysis_show_blocked_result()); + glean::content_analysis::default_result.Set( + StaticPrefs::browser_contentanalysis_default_result()); + glean::content_analysis::timeout_result.Set( + StaticPrefs::browser_contentanalysis_timeout_result()); + if (!clientSignature.IsEmpty()) { + glean::content_analysis::client_signature.Set( + NS_ConvertUTF16toUTF8(clientSignature)); + } + glean::content_analysis::bypass_for_same_tab_operations.Set( + StaticPrefs::browser_contentanalysis_bypass_for_same_tab_operations()); + { + nsCString allowUrlRegexList; + Preferences::GetCString(kAllowUrlPref, allowUrlRegexList); + // Unfortunately because of the way enterprise policies set and lock prefs, + // we can't check if the value is different than the default in + // StaticPrefList.yaml, and instead we have to duplicate that value here. At + // least we have a test around this so we can update this value if the + // default changes. + const char* defaultAllowUrlRegexList = "^about:(?!blank|srcdoc).*"; + glean::content_analysis::allow_url_regex_list_set.Set( + !allowUrlRegexList.Equals(defaultAllowUrlRegexList)); + } + { + nsCString denyUrlRegexList; + Preferences::GetCString(kDenyUrlPref, denyUrlRegexList); + glean::content_analysis::deny_url_regex_list_set.Set( + !denyUrlRegexList.IsEmpty()); + } +} + NS_IMETHODIMP ContentAnalysis::GetIsActive(bool* aIsActive) { *aIsActive = false; diff --git a/toolkit/components/contentanalysis/ContentAnalysis.h b/toolkit/components/contentanalysis/ContentAnalysis.h index cf2e9fc13a2f..4f404d99b762 100644 --- a/toolkit/components/contentanalysis/ContentAnalysis.h +++ b/toolkit/components/contentanalysis/ContentAnalysis.h @@ -306,6 +306,7 @@ class ContentAnalysis final : public nsIContentAnalysis, template RefPtr> CallClientWithRetry( StaticString aMethodName, U&& aClientCallFunc); + void RecordConnectionSettingsTelemetry(const nsString& clientSignature); nsresult RunAnalyzeRequestTask( const RefPtr& aRequest, bool aAutoAcknowledge, diff --git a/toolkit/components/contentanalysis/metrics.yaml b/toolkit/components/contentanalysis/metrics.yaml new file mode 100644 index 000000000000..f05a192ecdd9 --- /dev/null +++ b/toolkit/components/contentanalysis/metrics.yaml @@ -0,0 +1,152 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Adding a new metric? We have docs for that! +# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - 'Firefox :: Data Loss Prevention' + +# The following metrics are used to track the usage of the Data Loss Prevention feature. + +content_analysis: + agent_name: + type: string + description: The name of the DLP agent that Firefox is + connected to. This is set via enterprise policy. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com + interception_points_turned_off: + type: string_list + description: The interception points that are turned off + via enterprise policy. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com + show_blocked_result: + type: boolean + description: The show_blocked_result pref that is set + via enterprise policy. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com + default_result: + type: quantity + description: The default_result pref that is set + via enterprise policy. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com + unit: BlockWarnAllow + timeout_result: + type: quantity + description: The timeout_result pref that is set + via enterprise policy. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com + unit: BlockWarnAllow + client_signature: + type: string + description: The client_signature pref that is set + via enterprise policy. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com + bypass_for_same_tab_operations: + type: boolean + description: The bypass_for_same_tab_operations pref that is set + via enterprise policy. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com + allow_url_regex_list_set: + type: boolean + description: Whether the allow_url_regex_list pref is set + to a non-default value. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com + deny_url_regex_list_set: + type: boolean + description: Whether the deny_url_regex_list pref is set + to a non-default value. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com diff --git a/toolkit/components/contentanalysis/nsIContentAnalysis.idl b/toolkit/components/contentanalysis/nsIContentAnalysis.idl index 1d4bd32edff8..2f6d3805f1c8 100644 --- a/toolkit/components/contentanalysis/nsIContentAnalysis.idl +++ b/toolkit/components/contentanalysis/nsIContentAnalysis.idl @@ -509,4 +509,8 @@ interface nsIContentAnalysis : nsISupports */ void sendCancelToAgent(in ACString aUserActionId); + /** + * Force the content analysis client to be recreated. For use in tests only. + */ + void forceRecreateClientForTest(); }; diff --git a/toolkit/components/contentanalysis/tests/browser/browser_content_analysis_policies.js b/toolkit/components/contentanalysis/tests/browser/browser_content_analysis_policies.js index 3c541c92510a..738825af644b 100644 --- a/toolkit/components/contentanalysis/tests/browser/browser_content_analysis_policies.js +++ b/toolkit/components/contentanalysis/tests/browser/browser_content_analysis_policies.js @@ -46,6 +46,12 @@ const kInterceptionPoints = [ "file_upload", "print", ]; +// Everything is on by default except download. +let kInterceptionPointsOnByDefault = kInterceptionPoints.slice(); +kInterceptionPointsOnByDefault.splice( + kInterceptionPointsOnByDefault.indexOf("download"), + 1 +); const kInterceptionPointsPlainTextOnly = ["clipboard", "drag_and_drop"]; const ca = Cc["@mozilla.org/contentanalysis;1"].getService( @@ -111,7 +117,7 @@ add_task(async function test_ca_active() { PoliciesPrefTracker.stop(); }); -add_task(async function test_ca_enterprise_config() { +add_task(async function test_ca_enterprise_config_with_default_prefs() { PoliciesPrefTracker.start(); await EnterprisePolicyTesting.setupPolicyEngineWithJson({ policies: { @@ -150,6 +156,79 @@ add_task(async function test_ca_enterprise_config() { PoliciesPrefTracker.stop(); }); +add_task( + async function test_ca_enterprise_config_with_default_prefs_telemetry() { + PoliciesPrefTracker.start(); + Services.fog.testResetFOG(); + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + ContentAnalysis: { + Enabled: true, + }, + }, + }); + // Trigger the content analysis service to ensure telemetry is recorded. + let contentAnalysisService = Cc[ + "@mozilla.org/contentanalysis;1" + ].getService(Ci.nsIContentAnalysis); + is( + contentAnalysisService.isActive, + true, + "Content Analysis service is active" + ); + contentAnalysisService.forceRecreateClientForTest(); + + is( + Glean.contentAnalysis.agentName.testGetValue(), + "A DLP agent", + "agentName default" + ); + is( + Glean.contentAnalysis.interceptionPointsTurnedOff.testGetValue().length, + 1, + "interceptionPointsTurnedOff default" + ); + is( + Glean.contentAnalysis.interceptionPointsTurnedOff.testGetValue()[0], + "browser.contentanalysis.interception_point.download.enabled", + "interceptionPointsTurnedOff default" + ); + ok( + Glean.contentAnalysis.showBlockedResult.testGetValue(), + "showBlockedResult default" + ); + is( + Glean.contentAnalysis.defaultResult.testGetValue(), + 0, + "defaultResult default" + ); + is( + Glean.contentAnalysis.timeoutResult.testGetValue(), + 0, + "timeoutResult default" + ); + is( + Glean.contentAnalysis.clientSignature.testGetValue(), + null, + "clientSignature default" + ); + ok( + !Glean.contentAnalysis.bypassForSameTabOperations.testGetValue(), + "bypassForSameTabOperations default" + ); + ok( + !Glean.contentAnalysis.allowUrlRegexListSet.testGetValue(), + "allowUrlRegexListSet default" + ); + ok( + !Glean.contentAnalysis.denyUrlRegexListSet.testGetValue(), + "denyUrlRegexListSet default" + ); + + PoliciesPrefTracker.stop(); + } +); + add_task(async function test_ca_enterprise_config() { PoliciesPrefTracker.start(); const string1 = "this is a string"; @@ -277,6 +356,107 @@ add_task(async function test_ca_enterprise_config() { PoliciesPrefTracker.stop(); }); +add_task(async function test_ca_enterprise_config_telemetry() { + PoliciesPrefTracker.start(); + const string1 = "this is a string"; + const string2 = "this is another string"; + const string3 = "an agent name"; + const string4 = "a client signature"; + + Services.fog.testResetFOG(); + + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + ContentAnalysis: { + Enabled: true, + PipePathName: "abc", + AgentTimeout: 99, + AllowUrlRegexList: string1, + DenyUrlRegexList: string2, + AgentName: string3, + ClientSignature: string4, + IsPerUser: true, + MaxConnectionsCount: 3, + ShowBlockedResult: false, + DefaultResult: 1, + TimeoutResult: 2, + BypassForSameTabOperations: true, + InterceptionPoints: { + Clipboard: { + Enabled: false, + PlainTextOnly: false, + }, + Download: { + Enabled: true, + }, + DragAndDrop: { + Enabled: false, + PlainTextOnly: false, + }, + FileUpload: { + Enabled: false, + }, + Print: { + Enabled: false, + }, + }, + }, + }, + }); + + // Trigger the content analysis service to ensure telemetry is recorded. + let contentAnalysisService = Cc["@mozilla.org/contentanalysis;1"].getService( + Ci.nsIContentAnalysis + ); + contentAnalysisService.forceRecreateClientForTest(); + is( + contentAnalysisService.isActive, + true, + "Content Analysis service is active" + ); + + is(Glean.contentAnalysis.agentName.testGetValue(), string3, "agentName"); + const interceptionPointsTurnedOff = + Glean.contentAnalysis.interceptionPointsTurnedOff.testGetValue(); + is( + interceptionPointsTurnedOff.length, + 4, + "interceptionPointsTurnedOff length" + ); + for (let i = 0; i < interceptionPointsTurnedOff.length; i++) { + is( + interceptionPointsTurnedOff[i], + `browser.contentanalysis.interception_point.${kInterceptionPointsOnByDefault[i]}.enabled`, + `interceptionPointsTurnedOff ${kInterceptionPointsOnByDefault[i]}` + ); + } + ok( + !Glean.contentAnalysis.showBlockedResult.testGetValue(), + "showBlockedResult" + ); + is(Glean.contentAnalysis.defaultResult.testGetValue(), 1, "defaultResult"); + is(Glean.contentAnalysis.timeoutResult.testGetValue(), 2, "timeoutResult"); + is( + Glean.contentAnalysis.clientSignature.testGetValue(), + string4, + "clientSignature" + ); + ok( + Glean.contentAnalysis.bypassForSameTabOperations.testGetValue(), + "bypassForSameTabOperations" + ); + ok( + Glean.contentAnalysis.allowUrlRegexListSet.testGetValue(), + "allowUrlRegexListSet" + ); + ok( + Glean.contentAnalysis.denyUrlRegexListSet.testGetValue(), + "denyUrlRegexListSet" + ); + + PoliciesPrefTracker.stop(); +}); + add_task(async function test_cleanup() { ca.testOnlySetCACmdLineArg(false); await EnterprisePolicyTesting.setupPolicyEngineWithJson({ diff --git a/toolkit/components/glean/metrics_index.py b/toolkit/components/glean/metrics_index.py index 24c14cb74f52..edd7dd27645e 100644 --- a/toolkit/components/glean/metrics_index.py +++ b/toolkit/components/glean/metrics_index.py @@ -141,6 +141,7 @@ firefox_desktop_metrics = [ "browser/modules/metrics.yaml", "dom/media/platforms/wmf/metrics.yaml", "services/fxaccounts/metrics.yaml", + "toolkit/components/contentanalysis/metrics.yaml", "toolkit/components/contentrelevancy/metrics.yaml", "toolkit/components/crashes/metrics.yaml", "toolkit/components/nimbus/metrics.yaml",