// Verifies that changes to blocklistState are correctly reported to telemetry. "use strict"; Services.prefs.setBoolPref("extensions.blocklist.useMLBF", true); // Set min version to 42 because the updater defaults to min version 42. createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42.0", "42.0"); // Use unprivileged signatures because the MLBF-based blocklist does not // apply to add-ons with a privileged signature. AddonTestUtils.usePrivilegedSignatures = false; const { Downloader } = ChromeUtils.importESModule( "resource://services-settings/Attachments.sys.mjs" ); const { TelemetryController } = ChromeUtils.importESModule( "resource://gre/modules/TelemetryController.sys.mjs" ); const { TelemetryTestUtils } = ChromeUtils.importESModule( "resource://testing-common/TelemetryTestUtils.sys.mjs" ); const ExtensionBlocklistMLBF = getExtensionBlocklistMLBF(); const EXT_ID = "maybeblockme@tests.mozilla.org"; // The addon blocked by the bloom filter (referenced by MLBF_RECORD). const EXT_BLOCKED_ID = "@blocked"; const EXT_BLOCKED_VERSION = "1"; const EXT_BLOCKED_SIGN_TIME = 12345; // Before MLBF_RECORD.generation_time. // To serve updates. const server = AddonTestUtils.createHttpServer(); const SERVER_BASE_URL = `http://127.0.0.1:${server.identity.primaryPort}`; const SERVER_UPDATE_PATH = "/update.json"; const SERVER_UPDATE_URL = `${SERVER_BASE_URL}${SERVER_UPDATE_PATH}`; // update is served via `server` over insecure http. Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false); async function assertEventDetails(expectedExtras) { if (!IS_ANDROID_BUILD) { const expectedEvents = expectedExtras.map(expectedExtra => { let { object, value, ...extra } = expectedExtra; return ["blocklist", "addonBlockChange", object, value, extra]; }); await TelemetryTestUtils.assertEvents(expectedEvents, { category: "blocklist", method: "addonBlockChange", }); } else { info( `Skip assertions on collected samples for addonBlockChange on android builds` ); } assertGleanEventDetails(expectedExtras); } async function assertGleanEventDetails(expectedExtras) { const snapshot = testGetValue(Glean.blocklist.addonBlockChange); if (expectedExtras.length === 0) { Assert.deepEqual(undefined, snapshot, "Expected zero addonBlockChange"); return; } Assert.equal( expectedExtras.length, snapshot?.length, "Number of addonBlockChange records" ); for (let i of expectedExtras.keys()) { let actual = snapshot[i].extra; // Glean uses snake_case instead of camelCase. let { blocklistState, ...expected } = expectedExtras[i]; expected.blocklist_state = blocklistState; Assert.deepEqual(expected, actual, `Expected addonBlockChange (${i})`); } } // Stage an update on the update server. The add-on must have been created // with update_url set to SERVER_UPDATE_URL. function setupAddonUpdate(addonId, addonVersion) { let updateXpi = AddonTestUtils.createTempWebExtensionFile({ manifest: { version: addonVersion, browser_specific_settings: { gecko: { id: addonId, update_url: SERVER_UPDATE_URL }, }, }, }); let updateXpiPath = `/update-${addonId}-${addonVersion}.xpi`; server.registerFile(updateXpiPath, updateXpi); AddonTestUtils.registerJSON(server, SERVER_UPDATE_PATH, { addons: { [addonId]: { updates: [ { version: addonVersion, update_link: `${SERVER_BASE_URL}${updateXpiPath}`, }, ], }, }, }); } async function tryAddonInstall(addonId, addonVersion) { let xpiFile = AddonTestUtils.createTempWebExtensionFile({ manifest: { version: addonVersion, browser_specific_settings: { gecko: { id: addonId, update_url: SERVER_UPDATE_URL }, }, }, }); const install = await promiseInstallFile(xpiFile, true); // Passing true to promiseInstallFile means that the xpi may not be installed // if blocked by the blocklist. In that case, |install| may be null. return install?.addon; } add_task(async function setup() { if (!IS_ANDROID_BUILD) { // FOG needs a profile directory to put its data in. do_get_profile(); // FOG needs to be initialized in order for data to flow. Services.fog.initializeFOG(); } await TelemetryController.testSetup(); // Disable the packaged record and attachment to make sure that the test // will not fall back to the packaged attachments. Downloader._RESOURCE_BASE_URL = "invalid://bogus"; await promiseStartupManager(); }); add_task(async function install_update_not_blocked_is_no_events() { resetBlocklistTelemetry(); // Install an add-on that is not blocked. Then update to the next version. let addon = await tryAddonInstall(EXT_ID, "0.1"); // Version "1" not blocked yet, but will be in the next test task. setupAddonUpdate(EXT_ID, "1"); let update = await AddonTestUtils.promiseFindAddonUpdates(addon); await promiseCompleteInstall(update.updateAvailable); addon = await AddonManager.getAddonByID(EXT_ID); equal(addon.version, "1", "Add-on was updated"); await assertEventDetails([]); }); add_task(async function blocklist_update_events() { resetBlocklistTelemetry(); const EXT_HOURS_SINCE_INSTALL = 4321; const addon = await AddonManager.getAddonByID(EXT_ID); addon.__AddonInternal__.installDate = addon.installDate.getTime() - 3600000 * EXT_HOURS_SINCE_INSTALL; await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [ { stash: { blocked: [`${EXT_ID}:1`], unblocked: [] }, stash_time: 123 }, { stash: { blocked: [`${EXT_ID}:2`], unblocked: [] }, stash_time: 456 }, ], }); await assertEventDetails([ { object: "blocklist_update", value: EXT_ID, blocklistState: "2", // Ci.nsIBlocklistService.STATE_BLOCKED addon_version: "1", signed_date: "0", hours_since: `${EXT_HOURS_SINCE_INSTALL}`, mlbf_last_time: "456", mlbf_generation: "0", mlbf_source: "unknown", }, ]); }); add_task(async function update_check_blocked_by_stash() { resetBlocklistTelemetry(); setupAddonUpdate(EXT_ID, "2"); let addon = await AddonManager.getAddonByID(EXT_ID); let update = await AddonTestUtils.promiseFindAddonUpdates(addon); // Blocks in stashes are immediately enforced by update checks. // Blocks stored in MLBFs are only enforced after the package is downloaded, // and that scenario is covered by update_check_blocked_by_stash elsewhere. equal(update.updateAvailable, false, "Update was blocked by stash"); await assertEventDetails([ { object: "addon_update_check", value: EXT_ID, blocklistState: "2", // Ci.nsIBlocklistService.STATE_BLOCKED addon_version: "2", signed_date: "0", hours_since: "-1", mlbf_last_time: "456", mlbf_generation: "0", mlbf_source: "unknown", }, ]); }); // Any attempt to re-install a blocked add-on should trigger a telemetry // event, even though the blocklistState did not change. add_task(async function reinstall_blocked_addon() { resetBlocklistTelemetry(); let blockedAddon = await AddonManager.getAddonByID(EXT_ID); equal( blockedAddon.blocklistState, Ci.nsIBlocklistService.STATE_BLOCKED, "Addon was initially blocked" ); let addon = await tryAddonInstall(EXT_ID, "2"); ok(!addon, "Add-on install should be blocked by a stash"); await assertEventDetails([ { // Note: installs of existing versions are observed as "addon_install". // Only updates after update checks are tagged as "addon_update". object: "addon_install", value: EXT_ID, blocklistState: "2", // Ci.nsIBlocklistService.STATE_BLOCKED addon_version: "2", signed_date: "0", hours_since: "-1", mlbf_last_time: "456", mlbf_generation: "0", mlbf_source: "unknown", }, ]); }); // For comparison with the next test task (database_modified), verify that a // regular restart without database modifications does not trigger events. add_task(async function regular_restart_no_event() { resetBlocklistTelemetry(); // Version different/higher than the 42.0 that was passed to createAppInfo at // the start of this test file to force a database rebuild. await promiseRestartManager("90.0"); await assertEventDetails([]); await promiseRestartManager(); await assertEventDetails([]); }); add_task(async function database_modified() { resetBlocklistTelemetry(); const EXT_HOURS_SINCE_INSTALL = 3; await promiseShutdownManager(); // Modify the addon database: blocked->not blocked + decrease installDate. let addonDB = await IOUtils.readJSON(gExtensionsJSON.path); let rawAddon = addonDB.addons[0]; equal(rawAddon.id, EXT_ID, "Expected entry in addonDB"); equal(rawAddon.blocklistState, 2, "Expected STATE_BLOCKED"); rawAddon.blocklistState = 0; // STATE_NOT_BLOCKED rawAddon.installDate = Date.now() - 3600000 * EXT_HOURS_SINCE_INSTALL; await IOUtils.writeJSON(gExtensionsJSON.path, addonDB); // Bump version to force database rebuild. await promiseStartupManager("91.0"); // Shut down because the database reconcilation blocks shutdown, and we want // to be certain that the process has finished before checking the events. await promiseShutdownManager(); await assertEventDetails([ { object: "addon_db_modified", value: EXT_ID, blocklistState: "2", // Ci.nsIBlocklistService.STATE_BLOCKED addon_version: "1", signed_date: "0", hours_since: `${EXT_HOURS_SINCE_INSTALL}`, mlbf_last_time: "456", mlbf_generation: "0", mlbf_source: "unknown", }, ]); resetBlocklistTelemetry(); await promiseStartupManager(); await assertEventDetails([]); }); add_task(async function install_replaces_blocked_addon() { resetBlocklistTelemetry(); let addon = await tryAddonInstall(EXT_ID, "3"); ok(addon, "Update supersedes blocked add-on"); await assertEventDetails([ { object: "addon_install", value: EXT_ID, blocklistState: "0", // Ci.nsIBlocklistService.STATE_NOT_BLOCKED addon_version: "3", signed_date: "0", hours_since: "-1", mlbf_last_time: "456", mlbf_generation: "0", mlbf_source: "unknown", }, ]); }); add_task(async function install_blocked_by_mlbf() { resetBlocklistTelemetry(); await ExtensionBlocklistMLBF._client.db.saveAttachment( ExtensionBlocklistMLBF.RS_ATTACHMENT_ID, { record: MLBF_RECORD, blob: await load_mlbf_record_as_blob() } ); await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [MLBF_RECORD], }); AddonTestUtils.certSignatureDate = EXT_BLOCKED_SIGN_TIME; let addon = await tryAddonInstall(EXT_BLOCKED_ID, EXT_BLOCKED_VERSION); AddonTestUtils.certSignatureDate = null; ok(!addon, "Add-on install should be blocked by the MLBF"); await assertEventDetails([ { object: "addon_install", value: EXT_BLOCKED_ID, blocklistState: "2", // Ci.nsIBlocklistService.STATE_BLOCKED addon_version: EXT_BLOCKED_VERSION, signed_date: `${EXT_BLOCKED_SIGN_TIME}`, hours_since: "-1", // When there is no stash at all, the MLBF's generation_time is used. mlbf_last_time: `${MLBF_RECORD.generation_time}`, mlbf_generation: `${MLBF_RECORD.generation_time}`, mlbf_source: "cache_match", }, ]); }); // A limitation of the MLBF-based blocklist is that it needs the add-on package // in order to check its signature date. // This part of the test verifies that installation of the add-on is blocked, // despite the update check tentatively accepting the package. // See https://bugzilla.mozilla.org/show_bug.cgi?id=1649896 for rationale. add_task(async function update_check_blocked_by_mlbf() { resetBlocklistTelemetry(); // Install a version that we can update, lower than EXT_BLOCKED_VERSION. let addon = await tryAddonInstall(EXT_BLOCKED_ID, "0.1"); setupAddonUpdate(EXT_BLOCKED_ID, EXT_BLOCKED_VERSION); AddonTestUtils.certSignatureDate = EXT_BLOCKED_SIGN_TIME; let update = await AddonTestUtils.promiseFindAddonUpdates(addon); ok(update.updateAvailable, "Update was not blocked by stash"); await promiseCompleteInstall(update.updateAvailable); AddonTestUtils.certSignatureDate = null; addon = await AddonManager.getAddonByID(EXT_BLOCKED_ID); equal(addon.version, EXT_BLOCKED_VERSION, "Add-on was updated"); equal( addon.blocklistState, Ci.nsIBlocklistService.STATE_BLOCKED, "Add-on is blocked" ); equal(addon.appDisabled, true, "Add-on was disabled because of the block"); await assertEventDetails([ { object: "addon_update", value: EXT_BLOCKED_ID, blocklistState: "2", // Ci.nsIBlocklistService.STATE_BLOCKED addon_version: EXT_BLOCKED_VERSION, signed_date: `${EXT_BLOCKED_SIGN_TIME}`, hours_since: "-1", mlbf_last_time: `${MLBF_RECORD.generation_time}`, mlbf_generation: `${MLBF_RECORD.generation_time}`, mlbf_source: "cache_match", }, ]); }); add_task(async function update_blocked_to_unblocked() { resetBlocklistTelemetry(); // was blocked in update_check_blocked_by_mlbf. let blockedAddon = await AddonManager.getAddonByID(EXT_BLOCKED_ID); // 3 is higher than EXT_BLOCKED_VERSION. setupAddonUpdate(EXT_BLOCKED_ID, "3"); AddonTestUtils.certSignatureDate = EXT_BLOCKED_SIGN_TIME; let update = await AddonTestUtils.promiseFindAddonUpdates(blockedAddon); ok(update.updateAvailable, "Found an update"); await promiseCompleteInstall(update.updateAvailable); AddonTestUtils.certSignatureDate = null; let addon = await AddonManager.getAddonByID(EXT_BLOCKED_ID); equal(addon.appDisabled, false, "Add-on was re-enabled after unblock"); await assertEventDetails([ { object: "addon_update", value: EXT_BLOCKED_ID, blocklistState: "0", // Ci.nsIBlocklistService.STATE_NOT_BLOCKED addon_version: "3", signed_date: `${EXT_BLOCKED_SIGN_TIME}`, hours_since: "-1", mlbf_last_time: `${MLBF_RECORD.generation_time}`, mlbf_generation: `${MLBF_RECORD.generation_time}`, mlbf_source: "cache_match", }, ]); });