Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* Any copyright is dedicated to the Public Domain.
"use strict";
// Integration test for the C++ WasmModuleBackend, the ContentAnalysis backend
// selected by browser.contentanalysis.use_wasm_backend. It serializes a request
// to the content_analysis SDK protobuf, resolves the request's content,
// and hands both to the in-process wasm DLP module via nsIContentAnalysisWasmRunner.
//
// The wasm module is bundled in a WebExtension and read through the real
// production path, exactly as test_wasm_runner.js does. This test depends on the
// example rule set hard-coded in WasmModuleBackend::BuildExampleRules (block
// uploads to cloud-storage domains, warn on AI domains).
// TODO - refactor the rules code so we can just specify them from the test here
const { AddonManager } = ChromeUtils.importESModule(
"resource://gre/modules/AddonManager.sys.mjs"
);
const REQUIRE_SIGNATURE_PREF =
"browser.contentanalysis.wasm_module_extension_require_signature";
// Prefs must be set before the ContentAnalysis service is first instantiated,
// since the backend is chosen once in its constructor.
Services.prefs.setBoolPref("browser.contentanalysis.use_wasm_backend", true);
Services.prefs.setBoolPref("browser.contentanalysis.enabled", true);
Services.prefs.setBoolPref(REQUIRE_SIGNATURE_PREF, false);
Services.prefs.setBoolPref(
"browser.contentanalysis.interception_point.file_upload.enabled",
true
);
Services.prefs.setBoolPref(
"browser.contentanalysis.interception_point.clipboard.enabled",
true
);
Services.prefs.setBoolPref(
"browser.contentanalysis.bypass_for_same_tab_operations",
false
);
Services.prefs.setStringPref(
"browser.contentanalysis.allow_url_regex_list",
""
);
Services.prefs.setStringPref("browser.contentanalysis.deny_url_regex_list", "");
const contentAnalysis = Cc["@mozilla.org/contentanalysis;1"].getService(
Ci.nsIContentAnalysis
);
// Build a plain-object nsIContentAnalysisRequest. ContentAnalysis fills in the
// request token, user action ID, and request count itself, so we leave those
// empty.
function makeRequest({
analysisType,
reason,
operationTypeForDisplay,
urlSpec,
filePath = "",
textContent = "",
fileNameForDisplay = "",
printData = [],
printerName = "",
}) {
return {
analysisType,
reason,
operationTypeForDisplay,
fileNameForDisplay,
url: Services.io.newURI(urlSpec),
filePath,
textContent,
resources: [],
email: "",
sha256Digest: "",
requestToken: "",
userActionId: "",
userActionRequestsCount: 0,
timeoutMultiplier: 0,
getPrintData: () => printData,
printerName,
dataTransfer: null,
transferable: null,
windowGlobalParent: null,
sourceWindowGlobal: null,
testOnlyIgnoreCanceledAndAlwaysSubmitToAgent: false,
};
}
// Write a temp file with the given contents and return its absolute path,
// registering cleanup.
async function makeTempFile(name, contents) {
const file = do_get_tempdir();
file.append(name);
await IOUtils.writeUTF8(file.path, contents);
registerCleanupFunction(async () => {
await IOUtils.remove(file.path, { ignoreAbsent: true });
});
return file.path;
}
add_setup(async function () {
contentAnalysis.testOnlySetCACmdLineArg(true);
Assert.ok(
contentAnalysis.isActive,
"content analysis is active with the wasm backend"
);
registerCleanupFunction(() => {
contentAnalysis.testOnlySetCACmdLineArg(false);
});
});
// A file uploaded to a cloud-storage domain must be blocked. This exercises
// reading the file's contents off the main thread before handing them to the
// module.
add_task(async function test_file_upload_to_blocked_domain_is_blocked() {
const extension = await installModuleExtension();
const filePath = await makeTempFile(
"dlp_blocked_upload.txt",
"contents of a file being uploaded to cloud storage"
);
const result = await contentAnalysis.analyzeContentRequests(
[
makeRequest({
analysisType: Ci.nsIContentAnalysisRequest.eFileAttached,
reason: Ci.nsIContentAnalysisRequest.eFilePickerDialog,
operationTypeForDisplay: Ci.nsIContentAnalysisRequest.eUpload,
fileNameForDisplay: "dlp_blocked_upload.txt",
filePath,
}),
],
true
);
Assert.ok(
!result.shouldAllowContent,
"file upload to drive.google.com is blocked"
);
await extension.unload();
});
// The same file uploaded to an unlisted domain is allowed. This still reads the
// file off the main thread and round-trips it through the module.
add_task(async function test_file_upload_to_unlisted_domain_is_allowed() {
const extension = await installModuleExtension();
const filePath = await makeTempFile(
"dlp_allowed_upload.txt",
"contents of a file being uploaded to an ordinary site"
);
const result = await contentAnalysis.analyzeContentRequests(
[
makeRequest({
analysisType: Ci.nsIContentAnalysisRequest.eFileAttached,
reason: Ci.nsIContentAnalysisRequest.eFilePickerDialog,
operationTypeForDisplay: Ci.nsIContentAnalysisRequest.eUpload,
fileNameForDisplay: "dlp_allowed_upload.txt",
filePath,
}),
],
true
);
Assert.ok(result.shouldAllowContent, "file upload to example.com is allowed");
await extension.unload();
});
// An empty file must still round-trip cleanly (the off-main-thread read handles
// a zero-length file by passing empty content) and be allowed on an unlisted
// domain.
add_task(async function test_empty_file_upload_is_allowed() {
const extension = await installModuleExtension();
const filePath = await makeTempFile("dlp_empty_upload.txt", "");
const result = await contentAnalysis.analyzeContentRequests(
[
makeRequest({
analysisType: Ci.nsIContentAnalysisRequest.eFileAttached,
reason: Ci.nsIContentAnalysisRequest.eFilePickerDialog,
operationTypeForDisplay: Ci.nsIContentAnalysisRequest.eUpload,
fileNameForDisplay: "dlp_empty_upload.txt",
filePath,
}),
],
true
);
Assert.ok(result.shouldAllowContent, "empty file upload is allowed");
await extension.unload();
});
// Text (bulk data entry) requests take the synchronous, no-file path in the
// backend; verify it still works alongside the file path.
add_task(async function test_text_paste_to_unlisted_domain_is_allowed() {
const extension = await installModuleExtension();
const result = await contentAnalysis.analyzeContentRequests(
[
makeRequest({
analysisType: Ci.nsIContentAnalysisRequest.eBulkDataEntry,
reason: Ci.nsIContentAnalysisRequest.eClipboardPaste,
operationTypeForDisplay: Ci.nsIContentAnalysisRequest.eClipboard,
urlSpec: "https://example.com/",
textContent: "some pasted text",
}),
],
true
);
Assert.ok(result.shouldAllowContent, "text paste to example.com is allowed");
await extension.unload();
});
// Pasted text is handed to the module as content bytes, separately from the
// serialized request (see WasmModuleBackend::Analyze); the module's
// block-confidential-content rule (the only example rule keyed on content
// rather than domain) only triggers if those bytes actually reach it. The
// destination domain here isn't covered by any domain-based rule, isolating
// the content path from the domain path.
add_task(async function test_text_paste_with_confidential_marker_is_blocked() {
const extension = await installModuleExtension();
const result = await contentAnalysis.analyzeContentRequests(
[
makeRequest({
analysisType: Ci.nsIContentAnalysisRequest.eBulkDataEntry,
reason: Ci.nsIContentAnalysisRequest.eClipboardPaste,
operationTypeForDisplay: Ci.nsIContentAnalysisRequest.eClipboard,
urlSpec: "https://example.com/",
textContent: "top secret plan: CONFIDENTIAL launch details",
}),
],
true
);
Assert.ok(
!result.shouldAllowContent,
"text paste containing a CONFIDENTIAL marker is blocked"
);
await extension.unload();
});
// Same content-pattern rule, but content resolved from a file instead of
// inline text_content, to confirm the file-content path also reaches the
// module's pattern matching.
add_task(async function test_file_with_confidential_marker_is_blocked() {
const extension = await installModuleExtension();
const filePath = await makeTempFile(
"dlp_confidential.txt",
"top secret plan: CONFIDENTIAL launch details"
);
const result = await contentAnalysis.analyzeContentRequests(
[
makeRequest({
analysisType: Ci.nsIContentAnalysisRequest.eFileAttached,
reason: Ci.nsIContentAnalysisRequest.eFilePickerDialog,
operationTypeForDisplay: Ci.nsIContentAnalysisRequest.eUpload,
fileNameForDisplay: "dlp_confidential.txt",
filePath,
}),
],
true
);
Assert.ok(
!result.shouldAllowContent,
"file upload containing a CONFIDENTIAL marker is blocked"
);
await extension.unload();
});
// Print requests fetch their content via the cross-platform GetPrintData,
// unlike ExternalAgentBackend which (on the request-conversion path shared
// with the WASM backend) only knows how to ship print data via a Windows
// shared-memory handle. Verify the WASM backend correctly hands print data to
// the module on every platform.
add_task(async function test_print_to_unlisted_domain_is_allowed() {
const extension = await installModuleExtension();
const before = await contentAnalysis.getDiagnosticInfo();
const printData = Array.from(
new TextEncoder().encode("%PDF-1.4 fake print content for wasm test")
);
const result = await contentAnalysis.analyzeContentRequests(
[
makeRequest({
analysisType: Ci.nsIContentAnalysisRequest.ePrint,
reason: Ci.nsIContentAnalysisRequest.eSystemDialogPrint,
operationTypeForDisplay: Ci.nsIContentAnalysisRequest.eOperationPrint,
urlSpec: "https://example.com/",
printData,
printerName: "Test Printer",
}),
],
true
);
Assert.ok(result.shouldAllowContent, "print to example.com is allowed");
const after = await contentAnalysis.getDiagnosticInfo();
Assert.equal(
after.requestCount,
before.requestCount + 1,
"the print request reached the module instead of failing before " +
"it got there"
);
Assert.ok(
after.connectedToAgent,
"connected after analyzing a print request"
);
await extension.unload();
});
// GetDiagnosticInfo should track the number of analyze() calls and report
// that the module is connected after it runs successfully.
add_task(async function test_diagnostic_info_tracks_successful_analysis() {
const extension = await installModuleExtension();
const before = await contentAnalysis.getDiagnosticInfo();
await contentAnalysis.analyzeContentRequests(
[
makeRequest({
analysisType: Ci.nsIContentAnalysisRequest.eBulkDataEntry,
reason: Ci.nsIContentAnalysisRequest.eClipboardPaste,
operationTypeForDisplay: Ci.nsIContentAnalysisRequest.eClipboard,
urlSpec: "https://example.com/",
textContent: "some more pasted text",
}),
],
true
);
const after = await contentAnalysis.getDiagnosticInfo();
Assert.equal(
after.requestCount,
before.requestCount + 1,
"requestCount increases by one per analyze() call"
);
Assert.ok(after.connectedToAgent, "connected after a successful analysis");
Assert.ok(
!after.failedSignatureVerification,
"no signature failure after a successful analysis"
);
await extension.unload();
});
// A module extension that fails signature verification should be reflected in
// GetDiagnosticInfo, mirroring how ExternalAgentBackend reports a mismatched
// agent signature.
add_task(async function test_diagnostic_info_on_signature_failure() {
Services.prefs.setBoolPref(REQUIRE_SIGNATURE_PREF, true);
const extension = await installModuleExtension();
extension.extension.addonData.signedState = AddonManager.SIGNEDSTATE_MISSING;
await contentAnalysis.analyzeContentRequests(
[
makeRequest({
analysisType: Ci.nsIContentAnalysisRequest.eBulkDataEntry,
reason: Ci.nsIContentAnalysisRequest.eClipboardPaste,
operationTypeForDisplay: Ci.nsIContentAnalysisRequest.eClipboard,
urlSpec: "https://example.com/",
textContent: "text that can't be analyzed",
}),
],
true
);
const info = await contentAnalysis.getDiagnosticInfo();
Assert.ok(
!info.connectedToAgent,
"not connected after a signature verification failure"
);
Assert.ok(
info.failedSignatureVerification,
"failedSignatureVerification is set after a signature verification " +
"failure"
);
Services.prefs.setBoolPref(REQUIRE_SIGNATURE_PREF, false);
await extension.unload();
});
add_task(async function test_succeeds_with_signature_check_if_signed() {
Services.prefs.setBoolPref(REQUIRE_SIGNATURE_PREF, true);
const extension = await installModuleExtension();
extension.extension.addonData.signedState = AddonManager.SIGNEDSTATE_SYSTEM;
await contentAnalysis.analyzeContentRequests(
[
makeRequest({
analysisType: Ci.nsIContentAnalysisRequest.eBulkDataEntry,
reason: Ci.nsIContentAnalysisRequest.eClipboardPaste,
operationTypeForDisplay: Ci.nsIContentAnalysisRequest.eClipboard,
urlSpec: "https://example.com/",
textContent: "text that can't be analyzed",
}),
],
true
);
const info = await contentAnalysis.getDiagnosticInfo();
Assert.ok(info.connectedToAgent, "connected after using a signed extension");
Assert.ok(
!info.failedSignatureVerification,
"failedSignatureVerification is not set after using a signed extension"
);
Services.prefs.setBoolPref(REQUIRE_SIGNATURE_PREF, false);
await extension.unload();
});